Ocak 10 2025

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

  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

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

Category: Genel | LEAVE A COMMENT