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? Hani 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… Ne yazık ki o günler geride kaldı. Modern yazılım dünyasında artık baş tacımız microservices mimarisi. Ancak bu mimarinin beraberinde getirdiği en büyük baş ağrısı, bir isteğin (request) sistem içinde kaybolup gitmesi. İşte tam bu noktada, grafana ekosisteminin parlayan yıldızı tempo ve endüstri standardı haline gelen opentelemetry ikilisi devreye giriyor. Bu yazıda, maliyet dostu ve yüksek performanslı tracing dünyasına adım atacağız.
Neden Grafana Tempo? Elasticsearch’ün Gözü Yaşlı
Piyasada Jaeger veya Zipkin gibi rüştünü ispatlamış tracing çözümleri zaten var. Peki 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 devasa 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. “Peki index yoksa trace’leri nasıl bulacağız?” dediğinizi duyar gibiyim. Tempo, keşif (discover) sürecini loglara ve metriklere devreder. Siz 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, devasa Elasticsearch cluster yönetme derdi yok, sadece 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 gidebileceği gibi, 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)
Lokalinizde bu mimariyi ayağa kaldırmak için minimal bir Docker Compose dosyası hazırlayalım. Bu setup içerisinde Tempo, OpenTelemetry Collector ve görselleştirme için Grafana yer alıyor.
Öncelikle 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 geldi en heyecanlı kısma. Uygulamamızın içinden nasıl trace üreteceğiz? Bu örnekte Go dilini kullanacağız, ancak mantık Java, Node.js veya Python’da da tamamen aynıdır. Uygulamanın amacı, bir HTTP isteği aldığında arka planda “db-query” adında sanal bir alt işlem (span) başlatıp bunu trace etmektir.
İş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 bunu aldı ve Tempo’ya başarıyla iletti. Şimdi bu verileri 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
Eğer uygulamanızın loglarında basılan bir Trace ID varsa, bunu doğrudan arama çubuğuna yazıp aratabilirsiniz. Ancak şu an elimizde ID yoksa ne yapacağız? Tempo veri kaynağında “Search” sekmesini kullanarak sistemdeki son trace’leri listeleyebilirsiniz.
Listeden bir trace seçtiğinizde, sağ tarafta harika bir şelale grafiği (waterfall chart) belirecektir. 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, ancak onu prod ortamında ayakta tutmak tecrübe ister. İşte kulağınıza küpe olması gereken birkaç kıdemli DevOps tavsiyesi:
- Sampling (Örnekleme) Oranını İyi 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 sadece 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!