Grafana Tempo ile Distributed Tracing: Mikroservislerde Samanyolu Rehberi
Selamlar kertenkerem.net okurları! Monolitik uygulamaların gözünü seveyim dediğiniz o günleri hatırlıyor musunuz? Tek bir log dosyasına tail -f atıp hata anında tüm akışı tereyağından kıl çeker gibi süzdüğümüz o konforlu günleri… O günler geride kaldı. Modern yazılımda artık baş tacımız microservices mimarisi. Bu mimarinin getirdiği en büyük baş ağrısı ise bir isteğin (request) sistem içinde kaybolup gitmesi. Tam bu noktada Grafana ekosisteminin parlayan yıldızı Tempo ile endüstri standardı haline gelen OpenTelemetry ikilisi devreye giriyor. Bu yazıda maliyet dostu ve yüksek performanslı bir tracing kurulumunu adım adım anlatıyoruz.
Neden Grafana Tempo? Elasticsearch’ün gözü yaşlı
Piyasada Jaeger veya Zipkin gibi rüştünü ispatlamış tracing çözümleri zaten var. Neden Tempo? Cevap basit: maliyet ve operasyonel kolaylık.
Geleneksel tracing araçları trace verilerini hızlıca arayabilmek için Elasticsearch, Cassandra veya Jaeger-ingester gibi büyük ve yönetimi zor veritabanlarına ihtiyaç duyar. Bu da prod ortamında ciddi bir disk ve RAM maliyeti demektir. Tempo ise ezber bozan bir felsefeyle geldi: “Ben trace index’lemiyorum.”
Tempo, trace verilerini doğrudan S3, GCS veya Azure Blob Storage gibi ucuz object storage çözümlerinde saklar. “Index yoksa trace’leri nasıl bulacağız?” dediğinizi duyar gibiyim. Tempo keşif (discover) sürecini loglara ve metriklere devreder. Loglarınızda (örneğin Grafana Loki üzerinde) bir trace_id bulursunuz, bu ID’yi Tempo’ya sorarsınız ve Tempo nesne depolama alanından ilgili trace objesini saniyeler içinde çeker. Index yok, koca bir Elasticsearch cluster yönetme derdi yok, yalnızca saf Trace ID araması var.
Büyük resim: distributed tracing mimarisi nasıl çalışır?
Kuruluma geçmeden önce mimariyi kafamızda netleştirelim. Uygulamamızdan çıkan trace verileri doğrudan Tempo’ya gidebilir, ama en doğru pratik araya bir ajan (collector) koymaktır.
[Uygulama (OpenTelemetry SDK)]
│ (gRPC / HTTP - OTLP)
▼
[OpenTelemetry Collector]
│ (Batching, Filtering)
▼
[Grafana Tempo] ───> [Object Storage (S3 / Local Disk)]
▲
│ (Query via Trace ID)
[Grafana Explore]
Adım 1: oyun alanını kuralım (Docker Compose)
Bu mimariyi lokalinizde ayağa kaldırmak için minimal bir Docker Compose dosyası hazırlayalım. Bu setup içinde Tempo, OpenTelemetry Collector ve görselleştirme için Grafana yer alıyor.
Önce projenizin kök dizininde docker-compose.yml dosyasını oluşturalım:
version: '3.8'
services:
# 1. Grafana Tempo (Trace Deposu)
tempo:
image: grafana/tempo:latest
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./tempo-config.yaml:/etc/tempo.yaml
- ./tempo-data:/var/tempo
ports:
- "3200:3200" # Tempo API
- "4317:4317" # OTLP gRPC portu
# 2. OpenTelemetry Collector (Trafik Polisi)
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4318:4318" # OTLP HTTP portu
depends_on:
- tempo
# 3. Grafana (Görselleştirme)
grafana:
image: grafana/grafana:latest
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
ports:
- "3000:3000"
depends_on:
- tempo
Şimdi de Tempo’nun local diskte çalışabilmesi için basit bir konfigürasyon dosyası olan tempo-config.yaml dosyasını tanımlayalım:
stream_over_http: true
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
max_block_duration: 5m
storage:
trace:
backend: local
local:
path: /var/tempo/wal
wal:
path: /var/tempo/wal
compactor:
compaction:
block_Retention: 24h
Son olarak OpenTelemetry Collector’ın gelen trace’leri alıp Tempo’ya yönlendirmesini sağlayacak otel-collector-config.yaml dosyasını hazırlayalım:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
Adım 2: uygulama enstrümantasyonu (Go ile OpenTelemetry SDK)
Sıra en heyecanlı kısımda: Uygulamamızın içinden nasıl trace üreteceğiz? Bu örnekte Go kullanacağız, ama mantık Java, Node.js veya Python’da da aynıdır. Uygulama bir HTTP isteği aldığında arka planda “db-query” adında sanal bir alt işlem (span) başlatıp bunu trace edecek.
İşte main.go içeriğimiz:
package main
import (
"context"
"log"
"net/http"
"time"
"go.opentelemetry.io/otel"
"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.4.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
)
const (
serviceName = "kertenkerem-order-service"
collectorURL = "localhost:4317"
)
func initTracer() (*sdktrace.TracerProvider, error) {
ctx := context.Background()
// OTLP gRPC exporter kurulumu (OTel Collector'a göndermek için)
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithEndpoint(collectorURL),
otlptracegrpc.WithDialOption(grpc.WithBlock()),
)
if err != nil {
return nil, err
}
resources, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()), // Prod ortamında oran düşürülmeli!
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resources),
)
otel.SetTracerProvider(tp)
return tp, nil
}
func main() {
tp, err := initTracer()
if err != nil {
log.Fatalf("Tracer başlatılamadı: %v", err)
}
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Printf("Tracer kapatılırken hata oluştu: %v", err)
}
}()
tracer := otel.Tracer("http-server")
http.HandleFunc("/order", func(w http.ResponseWriter, r *http.Request) {
// Parent span başlatılıyor
ctx, span := tracer.Start(r.Context(), "ReceiveOrderRequest")
defer span.End()
// DB sorgusunu simüle eden alt span (child span)
queryDatabase(ctx, tracer)
w.Write([]byte("Sipariş başarıyla alındı!"))
})
log.Println("Server 8080 portunda çalışıyor...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func queryDatabase(ctx context.Context, tracer trace.Tracer) {
_, span := tracer.Start(ctx, "QueryDatabaseSpan")
defer span.End()
// Veritabanı gecikmesini simüle edelim
time.Sleep(150 * time.Millisecond)
}
Bu kodu çalıştırmadan önce docker-compose servislerinizi ayağa kaldırın:
docker-compose up -d
Ardından Go uygulamanızı çalıştırın ve curl ile birkaç istek göndererek trace üretin:
go run main.go
# Başka bir terminalden istek atın:
curl http://localhost:8080/order
Adım 3: Grafana Explore üzerinde trace görselleştirme
Trace verilerimizi ürettik, Collector bunları aldı ve Tempo’ya başarıyla iletti. Şimdi görselleştirme zamanı.
- Tarayıcınızdan http://localhost:3000 adresine giderek Grafana’ya giriş yapın.
- Sol menüden Connections -> Data Sources sekmesine gidin.
- Add data source butonuna tıklayın ve listeden Tempo’yu seçin.
- URL kısmına
http://tempo:3200yazın. Başka hiçbir ayara dokunmadan sayfanın altındaki Save & Test butonuna basın. “Data source is working” onayını görmelisiniz. - Sol menüden Explore sekmesine geçin ve veri kaynağı olarak üst kısımdan oluşturduğunuz Tempo’yu seçin.
Trace ID ile sorgulama yapmak
Uygulamanızın loglarında basılan bir Trace ID varsa bunu doğrudan arama çubuğuna yazıp aratabilirsiniz. Şu an elimizde ID yoksa Tempo veri kaynağındaki “Search” sekmesini kullanarak sistemdeki son trace’leri listeleyebilirsiniz.
Listeden bir trace seçtiğinizde sağ tarafta bir şelale grafiği (waterfall chart) belirecek. Bu grafikte ReceiveOrderRequest işleminin toplamda ne kadar sürdüğünü ve alt işlemi olan QueryDatabaseSpan’ın 150ms boyunca sistemi nasıl beklettiğini milisaniye hassasiyetinde görebilirsiniz.
DevOps pratikleri: prod ortamında dikkat edilmesi gerekenler
Distributed tracing kurmak kolaydır, ama onu prod ortamında ayakta tutmak tecrübe ister. Kulağınıza küpe olması gereken birkaç kıdemli DevOps tavsiyesi:
- Sampling (örnekleme) oranını iyi ayarlayın: Kodumuzda
AlwaysSample()kullandık. Bu, gelen her isteğin kaydedilmesi demektir. Saniyede 5000 istek alan bir prod ortamında bunu yaparsanız diskleri elinize alırsınız. Prod ortamında bu oranı %1 ile %5 arasına çekmelisiniz. Ya da tail-based sampling kullanarak yalnızca hata alan (5xx status code dönen) trace’leri kaydetmesini Collector seviyesinde yapılandırabilirsiniz. - Context propagation’ı unutmayın: Mikroservisler birbirini HTTP ya da gRPC ile ararken Trace ID bilgisini header’da taşımalıdır (W3C Trace Context standardı). Go tarafında
otel.SetTextMapPropagatorkullanarak bu akışın kesilmemesini sağlayın. - Logs-to-traces bağlantısı: Loki ve Tempo’yu birbirine bağlayın. Grafana’da log satırındaki Trace ID’ye tıklandığında doğrudan yan panelde Tempo trace grafiğinin açılması, operasyon ekibinizin hata çözme süresini (MTTR) saatlerden saniyelere indirecektir.
Mikroservis mimarisindeki karanlık noktaları aydınlatmak işte bu kadar kolay. Tempo ile hem bütçenizi koruyun hem de observability dünyasının nimetlerinden faydalanın. Bir sonraki teknik yazıda görüşmek üzere, sistemleriniz ayakta, gecikmeleriniz (latency) düşük olsun!