kertenkerem_
· DevOps · 5 dk okuma

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ı.

  1. Tarayıcınızdan http://localhost:3000 adresine giderek Grafana’ya giriş yapın.
  2. Sol menüden Connections -> Data Sources sekmesine gidin.
  3. Add data source butonuna tıklayın ve listeden Tempo’yu seçin.
  4. URL kısmına http://tempo:3200 yazın. Başka hiçbir ayara dokunmadan sayfanın altındaki Save & Test butonuna basın. “Data source is working” onayını görmelisiniz.
  5. 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.SetTextMapPropagator kullanarak 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!