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

Category: Genel | LEAVE A COMMENT
Ağustos 9 2024

Grafana Loki ile Merkezi Log Yönetimi: ELK’nın Hantallığından Kaçış Rehberi

Sistem yöneticilerinin ve DevOps mühendislerinin ortak rüyası nedir? Tabii ki gece yarısı “Disk Full” uyarısıyla uyanmamak. Ancak modern mikroservis mimarilerinde observability sağlamak ve devasa log yığınları arasında arama yapmak tam bir karın ağrısı olabiliyor. Yıllarca ELK (Elasticsearch, Logstash, Kibana) üçlüsünün JVM bellek canavarlığıyla mücadele ettikten sonra, hafif, dinamik ve bütçe dostu bir alternatif arıyorsanız doğru yerdesiniz. Bu rehberde, grafana ekosisteminin parlayan yıldızı loki ile nasıl ölçeklenebilir ve az maliyetli bir log management altyapısı kuracağımızı adım adım inceleyeceğiz.

Neden Loki? (Yani, Neden Elasticsearch Değil?)

Elasticsearch harika bir arama motorudur; bunu kimse inkar edemez. Ancak log yönetimi söz konusu olduğunda, Elasticsearch her şeyi (log satırının tamamını) indexler. Bu durum devasa index boyutlarına ve dolayısıyla korkunç bir RAM tüketimine yol açar. Kendi sunucularında ELK koşturanlar, Elasticsearch pod’unun 16GB RAM’i “kahvaltı niyetine” tükettiğini çok iyi bilir.

Loki ise farklı bir felsefe benimser: “Prometheus, ama loglar için.”

Loki, log içeriğinin tamamını indexlemez. Sadece logların metadata’sını (yani label/etiketlerini; örneğin app="nginx", env="production") indexler. Log satırlarının kendisini ise sıkıştırılmış chunk’lar halinde doğrudan nesne depolama servislerinde (AWS S3, Google Cloud Storage veya lokalde MinIO) saklar. Bu yaklaşım bize ne kazandırır?

  • Düşük Altyapı Maliyeti: RAM tüketimi MB’lar seviyesine düşer, disk maliyeti ise S3 kullanımı sayesinde neredeyse devede kulak kalır.
  • Kolay Korelasyon: Prometheus metriklerinizle aynı etiketleri (labels) kullanarak metrikten loga tek tıkla geçiş yapabilirsiniz.
  • Kolay Ölçeklenme: Loki’nin mikroservis mimarisi sayesinde yazma (ingester) ve okuma (querier) katmanlarını birbirinden bağımsız olarak ölçekleyebilirsiniz.

Sistem Mimarisi: Parçaları Birleştirelim

Loki tek başına çalışmaz. Log yönetim sistemimiz üç temel bileşenden oluşur:

  1. Promtail (Agent): Logları toplar, etiketler (labeling) ve Loki’ye push eder (Kubernetes dünyasında her node üzerinde bir DaemonSet olarak koşar).
  2. Loki (Veri Deposu): Gelen logları alır, indeksler ve depolar.
  3. Grafana (Görselleştirme): Loki’yi bir datasource olarak ekler, LogQL sorguları yazar ve şık dashboard’lar oluştururuz.

Adım Adım Kurulum: Kubernetes Üzerinde Loki ve Promtail

Lafı fazla uzatmadan klavyenin başına geçelim. Kurulum için en pratik yol Helm kullanmaktır. Kubernetes cluster’ınızın hazır olduğunu varsayarak işlemlere başlıyoruz.

1. Helm Depolarını Ekleme

İlk olarak Grafana’nın resmi Helm reposunu sistemimize ekleyelim ve güncelleyelim:

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

2. Loki Kurulumu

Üretim ortamı için Loki’yi S3 entegrasyonu ile kurmak en doğrusudur. Ancak bu rehberde pratiklik açısından lokal disk (StatefulSet) kullanan basit bir kurulum yapacağız. Aşağıdaki loki-values.yaml dosyasını oluşturalım:

loki:
  commonConfig:
    replication_factor: 1
  storage:
    type: 'filesystem'
  auth_enabled: false

singleBinary:
  replicas: 1

persistence:
  enabled: true
  size: 10Gi
  storageClassName: "standard" # Kendi storage class'ınızla değiştirin

Şimdi Loki’yi bu konfigürasyonla kuralım:

helm install loki grafana/loki -f loki-values.yaml --namespace logging --create-namespace

3. Promtail Kurulumu

Promtail, Kubernetes cluster’ındaki pod loglarını otomatik olarak keşfedecek şekilde konfigüre edilmiştir. promtail-values.yaml dosyamızı oluşturalım. Burada en kritik nokta Promtail’e Loki’nin adresini doğru vermektir:

config:
  clients:
    - url: http://loki-gateway.logging.svc.cluster.local/loki/api/v1/push

# Kubernetes namespace ve pod etiketlerini otomatik olarak loglara eklemesi için relabeling kuralları hazır gelir.

Promtail’i kuralım:

helm install promtail grafana/promtail -f promtail-values.yaml --namespace logging

LogQL 101: Loglar İçinde Kaybolmadan Arama Yapmak

Artık loglarımız Loki’ye akıyor. Peki bunları nasıl sorgulayacağız? Karşınızda LogQL. Eğer Prometheus’un sorgu dili olan PromQL’e aşinaysanız, LogQL size çocuk oyuncağı gelecektir. LogQL sorguları iki ana bölümden oluşur: Log Stream Selector ve Filter Expression.

Basit Filtreleme

Belirli bir namespace altındaki, belirli bir uygulamanın loglarını çekmek ve içinde “error” geçen satırları yakalamak için:

{namespace="production", app="payment-api"} |= "error"

Eğer “timeout” içeren ama “connection” içermeyen logları istiyorsak zincirleme filtre kullanabiliriz:

{app="nginx"} |= "timeout" != "connection"

Parser ve Yapılandırılmış (Structured) Loglar

Eğer uygulamanız JSON formatında log üretiyorsa, Loki bu logları çalışma anında (on-the-fly) ayrıştırabilir. Bu, Loki’nin en güçlü özelliklerinden biridir:

{app="auth-service"} | json | status = "500"

Yukarıdaki sorguda | json ifadesi, log satırını JSON olarak parse eder ve içerideki tüm JSON anahtarlarını geçici etiketlere (labels) dönüştürür. Sonrasında status = "500" filtresi ile sadece HTTP 500 hatası veren istekleri süzebiliriz.

Loglardan Metrik Üretmek

Diyelim ki son 5 dakikada Nginx loglarındaki HTTP 500 hatalarının saniyedeki oranını (rate) görmek istiyorsunuz. LogQL bunu yapabilir:

sum(rate({app="nginx"} | json | status =~ "5.." [5m]))

Bu sorguyu Grafana’da bir Graph panelinde çalıştırarak doğrudan loglardan türetilmiş canlı metrik grafikleri elde edebilirsiniz. Prometheus’a yük bindirmeden mükemmel bir alarm altyapısı!

Grafana ile Log Görselleştirme

Logları sorgulamak için Grafana arayüzünü kullanacağız. İşte izlemeniz gereken adımlar:

  1. Grafana arayüzüne gidin ve sol menüden Connections -> Data Sources yolunu izleyin.
  2. Add data source butonuna tıklayın ve listeden Loki‘yi seçin.
  3. Connection URL kısmına Loki servisinizin adresini yazın: http://loki-gateway.logging.svc.cluster.local (Eğer Grafana aynı cluster içindeyse).
  4. Sayfanın altındaki Save & Test butonuna tıklayın. Yeşil onay kodunu gördüyseniz bağlantı tamamdır!

Şimdi sol menüden Explore sekmesine gidin. Üst taraftan veri kaynağı olarak Loki‘yi seçin. Log browser üzerinden etiketlerinizi seçerek veya yukarıda paylaştığımız LogQL sorgularını yazarak loglarınızı canlı olarak (Live tailing) izlemeye başlayabilirsiniz.

Grafana Dashboard Visualization

Üretim Ortamı İçin Altın Tavsiyeler (Pro-Tips)

Loki’yi production ortamında koştururken canınızın sıkılmasını istemiyorsanız şu üç kurala mutlaka dikkat edin:

  1. Yüksek Cardinality’den Kaçının (High Cardinality): Loglarınıza asla user_id, ip_address veya request_id gibi binlerce benzersiz değere sahip dinamik etiketler (labels) eklemeyin. Bu, Loki’nin indeks yapısını şişirir ve sorgu performansını yerle bir eder. Bu tarz dinamik verileri log satırının içinde bırakın; aramak için LogQL filtrelerini (|= veya | json) kullanın.
  2. S3 Entegrasyonu: Lokal disk yerine her zaman AWS S3 veya MinIO gibi nesne depolama çözümlerini tercih edin. Bu sayede log retention (logların silinme süresi) politikanızı kolayca yönetebilir ve disk dolma riskini sıfıra indirebilirsiniz.
  3. Query Frontend Kullanımı: Yoğun log sorgulaması yapılan ortamlarda Loki’yi mikroservis modunda kurup query-frontend bileşenini aktif edin. Bu bileşen, büyük zaman aralıklarındaki sorguları parçalara ayırarak paralel çalıştırır ve caching mekanizması sunar.

Özet

Grafana Loki, modern DevOps pratiklerinde log yönetimi için ezber bozan bir araç. ELK’nın getirdiği o devasa operasyonel yük ve maliyet olmadan, sadece ihtiyacımız olanı indexleyerek harika bir observability altyapısı kurduk. Promtail ile logları topladık, LogQL ile derinlemesine analiz ettik ve Grafana ile taçlandırdık.

Siz de production ortamlarınızda kaynak tüketimini optimize etmek istiyorsanız, Loki’ye bir şans verin. Bir sonraki yazımızda görüşmek üzere, loglarınız temiz, sistemleriniz ayakta kalsın!

Category: Genel | LEAVE A COMMENT