Ağustos 23 2024

Grafana Tempo ve OpenTelemetry ile Distributed Tracing

Gece saat 03:00. Telefonunuz çalıyor, PagerDuty çığlık çığlığa. Prod ortamındaki o kritik ödeme servisi yavaşlamış, hata oranları tavan yapmış durumda. Hemen bilgisayarınızı açıp loglara bakıyorsunuz: Dev bir 500 Internal Server Error yığını. Ancak bu hata, o servisin kendisinden mi kaynaklanıyor, yoksa arkada çağırdığı 15 farklı microservice’ten birinin database sorgusunun takılmasından mı? İşte bu noktada loglar yetersiz, metrikler ise dilsiz kalır. İhtiyacımız olan şey, bir isteğin sistemdeki tüm yolculuğunu uçtan uca görebilmektir.

Bu yazıda, modern gözlemlenebilirlik (observability) dünyasının kutsal üçlüsünden biri olan distributed tracing konusunu ele alacağız. Eski nesil, yönetmesi ve ölçeklemesi tam bir operasyonel kabus olan tracing çözümlerini bir kenara bırakıp; Grafana, Tempo ve OpenTelemetry (OTel) kullanarak, production-ready, maliyet dostu ve yüksek performanslı bir tracing hattını nasıl kuracağımızı adım adım inceleyeceğiz. Üstelik bunu yaparken, microservices mimarimizin performans darboğazlarını nasıl saniyeler içinde tespit edeceğimizi göreceğiz.

Neden Grafana Tempo? Elasticsearch’e RAM Yetiştiremeyenler Kulübü

Eğer daha önce Jaeger veya Zipkin kullanarak bir distributed tracing altyapısı kurduysanız, arka planda devasa bir Elasticsearch veya Cassandra cluster’ı yönetmenin ne kadar sancılı olduğunu bilirsiniz. Sadece trace datalarını indekslemek için harcanan RAM ve CPU miktarı, bazen uygulamanın kendisinden fazla kaynak tüketebilir.

Grafana Tempo, bu probleme radikal bir mühendislik yaklaşımı getiriyor: “No-index” architecture. Tempo, trace datalarını indekslemez. Bunun yerine, trace’leri sadece Trace ID’lerine göre anahtarlayarak nesne depolama servislerinde (Object Storage – AWS S3, Google Cloud Storage veya lokalde MinIO) bloklar halinde saklar. Peki, indeksleme yoksa arama nasıl yapılıyor? Tempo, metrikler (Prometheus) ve loglar (Loki) arasındaki korelasyonu kullanır. Logunuzda bir Trace ID bulursunuz ve Tempo bu ID’yi doğrudan nesne deposundan milisaniyeler içinde çeker. Sonuç? Sıfıra yakın operasyonel maliyet ve inanılmaz ucuz storage faturaları.

Mimarinin Tasarımı: Veri Nasıl Akacak?

Uygulamamızdan çıkan trace verilerinin Tempo’ya ulaşması için endüstri standardı olan OpenTelemetry protokolünü (OTLP) kullanacağız. Doğrudan uygulamadan Tempo’ya yazmak yerine, araya bir OpenTelemetry Collector koyacağız. Neden mi? Çünkü collector, uygulamalarımızın üzerindeki yükü alır, verileri buffer’lar, batch halinde gönderir ve gerekirse hassas verileri (PII) maskeleme işini üstlenir. Akış tam olarak şöyle olacak:

Uygulama (Go SDK) --[OTLP/gRPC]--> OTel Collector --[OTLP/gRPC]--> Grafana Tempo <-- Grafana (UI)

Adım 1: Altyapıyı Ayağa Kaldıralım (Docker Compose)

Lafı uzatmadan pratik tarafa geçelim. Lokalinizde bu yapıyı test edebilmeniz için hazırladığım, içinde Tempo, OTel Collector ve Grafana bulunan docker-compose.yml dosyamızı oluşturalım.

version: '3.8'

services:
  # Grafana Tempo (Trace Deposu)
  tempo:
    image: grafana/tempo:2.3.0
    command: [ "-config.file=/etc/tempo.yaml" ]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
    ports:
      - "3200:3200"   # HTTP API
      - "4317:4317"   # OTLP gRPC receiver

  # OpenTelemetry Collector (Veri Toplayıcı ve Dağıtıcı)
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.90.0
    command: [ "--config=/etc/otel-collector-config.yaml" ]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4318:4318"   # OTLP HTTP receiver
    depends_on:
      - tempo

  # Grafana (Görselleştirme)
  grafana:
    image: grafana/grafana:10.2.0
    volumes:
      - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
    ports:
      - "3000:3000"
    depends_on:
      - tempo

Şimdi Tempo’nun konfigürasyon dosyası olan tempo.yaml dosyasını oluşturalım. Burada lokal diskimizi bir object storage gibi simüle edeceğiz:

stream_over_http: true
server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317

ingester:
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo/traces
    wal:
      path: /tmp/tempo/wal

Sırada OTel Collector konfigürasyonumuz (otel-collector-config.yaml) var. Burası gelen trace verisini karşılayıp Tempo’ya paslayacak olan ana santralimiz:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 256

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

Son olarak Grafana’nın ayağa kalktığında otomatik olarak Tempo veri kaynağını tanıması için grafana-datasources.yaml dosyasını hazırlayalım:

apiVersion: 1

datasources:
  - name: Tempo
    type: tempo
    access: proxy
    orgId: 1
    url: http://tempo:3200
    basicAuth: false
    isDefault: true
    uid: tempo-datasource

Adım 2: Uygulama Enstrümantasyonu (Go ve OpenTelemetry)

Altyapımız hazır olduğuna göre, artık kod seviyesine inebiliriz. Distributed tracing’in en kritik kavramı Context Propagation‘dır. Yani bir istek dışarıdan geldiğinde oluşan Trace ID’nin, alt servislere yapılan HTTP veya gRPC çağrılarına taşınması gerekir. Eğer bu zinciri kırarsanız, trace bütünlüğünüz kaybolur.

Aşağıdaki Go örneğinde, hem bir OpenTelemetry Tracer’ı nasıl initialize edeceğimizi, hem de manuel olarak nasıl span üreteceğimizi göreceğiz. Bu örnek, prod ortamındaki mikroservisleriniz için harika bir şablon olacaktır.

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 = "payment-gateway"
	collectorAddr = "localhost:4317"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
	// gRPC üzerinden OTel Collector'a bağlanacak exporter
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithInsecure(),
		otlptracegrpc.WithEndpoint(collectorAddr),
		otlptracegrpc.WithDialOption(grpc.WithBlock()),
	)
	if err != nil {
		return nil, err
	}

	// Servis kimlik bilgileri
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String(serviceName),
			semconv.ServiceVersionKey.String("1.0.0"),
		),
	)
	if err != nil {
		return nil, err
	}

	// Tracer Provider tanımı (Samplers burada belirlenebilir)
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()), // Prod için ParentBased(AlwaysSample) önerilir
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	otel.SetTracerProvider(tp)
	return tp, nil
}

func main() {
	ctx := context.Background()
	tp, err := initTracer(ctx)
	if err != nil {
		log.Fatalf("Tracer başlatılamadı: %v", err)
	}
	defer func() {
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("Tracer kapatılırken hata: %v", err)
		}
	}()

	tracer := otel.Tracer("http-server")

	http.HandleFunc("/checkout", func(w http.ResponseWriter, r *http.Request) {
		// HTTP isteğinden gelen context'i ve trace parent bilgisini alıyoruz
		ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
		
		ctx, span := tracer.Start(ctx, "CheckoutProcess", trace.WithSpanKind(trace.SpanKindServer))
		defer span.End()

		// DB Sorgusunu simüle eden bir alt span oluşturalım
		_, dbSpan := tracer.Start(ctx, "QueryUserBalance")
		time.Sleep(120 * time.Millisecond) // Veritabanı gecikmesi
		dbSpan.SetAttributes(semconv.DBSystemPostgreSQL)
		dbSpan.End()

		// Üçüncü parti API çağrısını simüle edelim
		_, apiSpan := tracer.Start(ctx, "CallStripeAPI")
		time.Sleep(350 * time.Millisecond) // Dış servis gecikmesi
		apiSpan.End()

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status": "success"}`))
	})

	log.Println("Server 8080 portunda çalışıyor...")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Adım 3: Grafana Explore’da Trace Görselleştirme

Artık her şey hazır! Sistemimizi ayağa kaldırmak için terminalden şu komutu çalıştıralım:

docker compose up -d

Ardından Go uygulamamızı çalıştıralım ve test isteği gönderelim:

go run main.go
curl -i http://localhost:8080/checkout

Şimdi tarayıcınızdan http://localhost:3000 adresine giderek Grafana’ya giriş yapın (varsayılan kullanıcı adı/şifre: admin/admin). Sol menüden Explore sekmesine tıklayın ve veri kaynağı olarak Tempo‘yu seçin.

Burada Search sekmesine gelip “payment-gateway” servisimizi seçtiğimizde, az önce attığımız isteğin trace verisini göreceğiz. Trace’e tıkladığınızda karşınıza çıkacak olan Gantt şeması benzeri görselleştirme, size şu altın bilgileri sunacak:

  • İsteğin toplamda ne kadar sürdüğünü (örn: 470ms)
  • Hangi alt işlemin (QueryUserBalance vs CallStripeAPI) bu sürenin ne kadarını yediğini (Darboğaz tespiti!)
  • Eğer işlem sırasında bir hata oluştuysa, o hatanın tam olarak hangi span üzerinde patladığını ve hata detaylarını (Exception stack trace)

Pro Tip: Loglar ve Trace’leri Birleştirmek (Derived Fields)

Kıdemli bir DevOps mühendisinin fark yaratacağı yer burasıdır. Sadece trace izlemek yetmez, loglar ile trace’ler arasında köprü kurmalısınız. Grafana Loki konfigürasyonunuza ekleyeceğiniz bir derived fields kuralı ile log satırındaki trace_id değerini otomatik olarak Tempo’ya yönlendiren tıklanabilir linklere dönüştürebilirsiniz. Böylece hata loguna bakan bir yazılımcı, tek bir tıkla o hatanın oluştuğu trace’e sıçrayabilir. Buna observability dünyasında “correlating signals” denir.

Sonuç ve Production Tavsiyeleri

Uygulamalarınız büyüdükçe ve microservices sayınız arttıkça, distributed tracing lüks olmaktan çıkıp bir hayatta kalma aracına dönüşür. Grafana Tempo ve OpenTelemetry ikilisi, size hem açık standartlara dayalı (vendor lock-in olmadan) hem de son derece düşük maliyetli bir gözlemlenebilirlik platformu sunar.

Üretim ortamına geçişte şu noktalara dikkat etmenizi öneririm:

  • Sampling (Örnekleme): Her isteğin trace’ini saklamak disk alanınızı hızla tüketebilir. OTel Collector üzerinde tail-based sampling kullanarak sadece hatalı (5xx) veya uzun süren (latency > 2s) istekleri %100 saklayıp, başarılı isteklerin sadece %1’ini örnekleyebilirsiniz.
  • Resource Constraints: OTel Collector için mutlaka memory_limiter processor’ını aktif edin. Yoğun yük altında collector’ın memory sızıntısı yapmasını engellemiş olursunuz.

Sistemlerinizi izlenebilir kılın, geceleri rahat uyuyun!

Etiketler: , , , ,
Copyright 20254541. All rights reserved.

Posted 23 Ağustos 2024 by Kerem Danış in category "Genel