Spring Boot 4 Observability

OpenTelemetry ve Grafana ile uygulama performansını okumak

Bir Spring Boot 4 uygulamasında OpenTelemetry sinyallerini üretip Grafana üzerinde metrics, traces ve logs ile performans sorunlarını nasıl yorumlayacağını adım adım anlatan pratik rehber. Yazının teknik dayanakları ve resmi doküman bağlantıları en alttaki kaynakça bölümünde yer alıyor.

P95 LATENCY
342 ms
ERROR RATE
1.8%
TRACE WATERFALL
LOG CORRELATION
trace_id

Aynı request'in log, span ve metric bağlantısı.

1. Observability nedir ve monitoring'den farkı nedir?

Monitoring genelde "sistem ayakta mı, CPU kaç, memory kaç, hata var mı?" sorularına cevap verir. Observability ise daha ileri gider: sistemin iç durumunu dışarıdan üretilen sinyallerle anlamaya çalışır. Yani sadece alarm görmek değil, alarmın nedenini bulmak için iz bırakmaktır.

Spring Boot 4 tarafında bu yaklaşım Micrometer Observation, OpenTelemetry ve OTLP exporter akışıyla kurulabilir. Grafana tarafında ise bu sinyaller dashboard, trace waterfall, log araması ve alert panelleriyle okunur.

Metrics

Sayısal ölçümlerdir. Request süresi, hata oranı, JVM memory, thread sayısı gibi değerleri zaman içinde gösterir.

Traces

Bir isteğin servisler ve katmanlar arasında hangi adımlardan geçtiğini, her adımın kaç ms sürdüğünü gösterir.

Logs

Uygulamanın olay günlüğüdür. Hata detayı, iş akışı, parametre ve karar noktaları için bağlam sağlar.

2. Metrics, traces ve logs nasıl yorumlanır?

İyi bir teşhis akışı genelde metrics ile başlar, traces ile daralır, logs ile kanıtlanır. Örneğin Grafana'da P95 latency 300 ms'den 900 ms'ye çıktıysa önce hangi endpoint'in yavaşladığını görürsün. Sonra trace waterfall'da yavaş span'i bulursun. En son aynı `trace_id` ile loglara gidip sebebi okursun.

Latency değerlerini okuma

HTTP Controller
38 ms
Service Layer
64 ms
PostgreSQL
420 ms
External API
710 ms
Yorumlama mantığı

Ortalama latency tek başına yeterli değildir. P50 normal kalırken P95/P99 yükseliyorsa kullanıcıların küçük ama önemli bir kısmı ciddi yavaşlık yaşıyor olabilir. Grafikte ani spike varsa deploy, cache miss, database lock, thread pool saturation veya external API gecikmesi düşünülmelidir.

Metric okurken

  • P95/P99 latency trendine bak.
  • Error rate ile latency aynı anda yükseliyor mu kontrol et.
  • CPU düşük ama latency yüksekse I/O veya lock beklemesi olabilir.
  • Thread sayısı ve queue depth artıyorsa concurrency baskısı vardır.

Trace okurken

  • En uzun span'i bul.
  • DB, cache, HTTP client span'lerini ayrı ayrı incele.
  • Parent-child ilişkisini takip et.
  • Aynı trace içindeki error tag ve log correlation alanlarına bak.

3. Spring Boot 4 bazlı kurulum: dependency, OTLP endpoint ve Grafana stack

Spring Boot 4 ile temel yaklaşım şudur: uygulama telemetry sinyallerini OTLP formatında dışarı verir, OpenTelemetry Collector bu sinyalleri alır, Grafana ekosistemi ise metrics, traces ve logs tarafını görselleştirir. Basit lokal kurulumda Grafana, Tempo ve Loki yeterlidir; production'da collector mutlaka araya konulmalıdır.

Maven dependencySpring Boot 4.0.5
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.5</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-opentelemetry</artifactId>
    </dependency>
</dependencies>
application.ymlOTLP export
spring:
  application:
    name: finance-portal-api

management:
  opentelemetry:
    resource-attributes:
      service.name: finance-portal-api
      service.namespace: portfolio
      deployment.environment: local
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
    metrics:
      export:
        url: http://localhost:4318/v1/metrics
    logging:
      endpoint: http://localhost:4318/v1/logs
  observations:
    key-values:
      region: eu-central-1
      stack: local
OpenTelemetry Collectorotel-collector.yml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
  prometheus:
    endpoint: 0.0.0.0:9464

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/tempo]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]
Not

Spring Boot dokümantasyonu OpenTelemetry için farklı yollar olduğunu söyler: Java agent, OpenTelemetry Spring Boot starter veya Spring'in Micrometer/OTLP yaklaşımı. Spring Boot 4 örneğinde hedefimiz vendor-neutral telemetry üretmek ve Grafana tarafında yorumlamaktır.

4. Grafana'da süreç okuma: dashboard'dan trace'e, trace'den log'a

Grafana'da iyi bir inceleme akışı panikten uzak olmalıdır. Önce büyük resmi görürsün: request rate, error rate, latency histogramları, JVM memory, GC pause, active threads. Sonra problemli endpoint veya servis seçilir. Ardından trace detayına gidilir ve log korelasyonu yapılır.

1. Dashboard

Latency ve error rate spike'ını yakala. Hangi servis ve endpoint etkilenmiş bunu bul.

2. Trace

Request içindeki en uzun span'i bul. DB mi, external API mi, lock beklemesi mi ayır.

3. Log

Aynı trace id ile loglara git. Exception, retry, timeout veya business rule kararını oku.

Örnek yorum

Diyelim `/api/portfolio/summary` endpoint'i normalde P95 olarak 180 ms çalışıyor. Deploy sonrası P95 740 ms oldu. Trace waterfall'da controller 20 ms, service 45 ms ama PostgreSQL span'i 620 ms görünüyor. Bu durumda controller koduna değil, query plan, index, lock, connection pool ve N+1 sorgu ihtimaline odaklanırsın.

5. Multithreading ortamda izleme: context propagation neden kritik?

Observability verisi çoğu zaman `ThreadLocal` bağlamla taşınır: trace id, span id, baggage gibi bilgiler mevcut execution context içinde tutulur. Eğer iş başka thread'e atılırsa bu bağlam kaybolabilir. Sonuçta trace kopuk görünür, loglar trace id taşımayabilir ve Grafana'da request bütünlüğü bozulur.

Buradaki kritik ayrım şudur: aynı thread içinde devam eden iş ile yeni bir task olarak başka bir executor'a atılan iş aynı şey değildir. Eğer request tek bir virtual thread içinde başlıyor ve yine o virtual thread içinde bitiyorsa trace genelde korunur. Ama `@Async`, `CompletableFuture`, scheduler, custom executor veya fan-out yapan paralel task modeli devreye girdiğinde bağlamı açıkça taşımak gerekir.

Context propagationSpring Boot 4
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;

@Configuration(proxyBeanMethods = false)
class ObservabilityContextConfiguration {

    @Bean
    ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
        return new ContextPropagatingTaskDecorator();
    }
}
Neden önemli?

`@Async`, `AsyncTaskExecutor`, custom executor, scheduler veya virtual thread executor kullanırken request context başka çalışma hattına geçer. Context propagation yoksa aynı request'in parçaları Grafana'da ayrı olaylarmış gibi görünür.

ThreadLocal neden kaybolur?

OpenTelemetry ve Micrometer tracing tarafında aktif observation/span bilgisi çoğu zaman `ThreadLocal` üzerinden tutulur. `ThreadLocal` adı üstünde thread'e özeldir. Sen parent request içinde bir trace başlatıp sonra işi başka bir thread'e verdiğinde, yeni thread eski thread'in `ThreadLocal` değerlerini kendiliğinden almaz. Bu yüzden child task içinde span açıldığında parent trace görünmeyebilir.

Trace'in korunduğu durum

  • Aynı request aynı execution hattında ilerliyorsa
  • Spring managed executor üstünde task decorator varsa
  • Reactor context propagation doğru ayarlanmışsa

Trace'in koptuğu sınırlar

  • `@Async` method çağrısı
  • `CompletableFuture.supplyAsync(...)`
  • Elle açılmış executor veya scheduler
  • Virtual thread içine ayrı child task submit edilmesi

Virtual thread kullanınca ne değişir?

Virtual thread daha hafif bir çalışma modeli sunar ama context propagation problemini sihirli şekilde çözmez. Trace context virtual thread üzerinde de yine execution context olarak taşınır. Yani sorun "platform thread mi, virtual thread mi" sorusundan çok, işi yeni bir boundary'den geçiriyor musun? sorusudur.

Başka bir deyişle: tek request tek virtual thread üzerinde ilerliyorsa trace çoğu zaman kaybolmaz. Fakat o virtual thread içinde üç ayrı child task üretip bunları başka virtual thread'lere veya başka executor'a submit edersen, parent context'i çocuk işlere snapshot ile aktarman gerekir.

Virtual thread executor ile context taşımaMicrometer Context Propagation
import io.micrometer.context.ContextSnapshotFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadTracingExample {

    private final ContextSnapshotFactory contextSnapshotFactory =
        ContextSnapshotFactory.builder().build();

    void runTasks() {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            Runnable task = contextSnapshotFactory.captureAll().wrap(() -> {
                // Bu blokta parent trace / observation context yeniden kurulmuş olur.
                callRemoteService();
            });

            executor.submit(task);
        }
    }
}
Bu örnekte ne oluyor?

`captureAll()` mevcut thread'deki context'i snapshot olarak alır. `wrap(...)` ise bu context'i child task çalışırken yeniden kurar. Böylece child task ister platform thread'de ister virtual thread'de koşsun, trace zinciri parent request ile bağlı kalır.

Spring tarafında nasıl çözülür?

Spring Boot 4 observability dokümanında önerilen yaklaşım, framework tarafından kullanılan async executor'larda `ContextPropagatingTaskDecorator` tanımlamaktır. Bu özellikle `@Async` ve `AsyncTaskExecutor` kullanan uygulamalarda en pratik çözümdür.

Virtual thread task executor beanSpring Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskExecutor;

@Configuration(proxyBeanMethods = false)
class AsyncExecutorConfiguration {

    @Bean
    AsyncTaskExecutor applicationTaskExecutor() {
        SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("app-vt-");
        executor.setVirtualThreads(true);
        executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
        return executor;
    }
}

Bu bean ile Spring'in async sınırından geçen işler virtual thread üzerinde çalışabilir ve aynı anda observation context de korunur. Böylece Grafana trace ekranında `@Async` ile ayrılmış child span'ler kopuk görünmez.

İzlenecek metrikler

  • Active threads ve queued tasks
  • Executor queue wait time
  • Task duration histogram
  • Rejected task count
  • Lock wait veya DB connection wait

Trace tarafında bakılacaklar

  • Async span parent-child ilişkisi korunmuş mu?
  • Thread geçişinden sonra trace id aynı mı?
  • Virtual thread fan-out sonrası child span'ler aynı trace altında mı?
  • Long-running task ayrı span olarak görünüyor mu?
  • Timeout/retry span event olarak işlenmiş mi?

6. Best practices: production'da okunabilir telemetry üretmek

Service name standardı koy `service.name`, `service.namespace`, `deployment.environment`, `region` gibi alanlar her serviste tutarlı olmalı. Yoksa dashboard filtreleri anlamsızlaşır.
High cardinality tag'leri metrics'e koyma `userId`, `orderId`, `traceId` gibi değerler metrics label olursa zaman serisi patlar. Bunları trace/log tarafında kullan.
Latency histogramlarını endpoint bazlı oku Sadece global latency bakma. `/login`, `/portfolio/summary`, `/orders` ayrı davranır.
Logs, traces ve metrics'i korele et Log formatında trace id ve span id varsa Grafana'da tek request'in tüm hikayesi okunur.
Alert'i semptoma, trace'i nedene bağla Alert "P95 800 ms oldu" der. Trace "PostgreSQL span'i 650 ms sürdü" diye nedeni daraltır.
En yaygın hata

Her şeyi loglamak observability değildir. Çok log üretmek maliyeti artırır ve problemi bulmayı zorlaştırır. İyi telemetry az ama anlamlı alanlarla, tutarlı isimlerle ve korelasyon id'leriyle üretilir.

Kaynakça

Bu yazıdaki Spring Boot 4 observability yaklaşımı, OpenTelemetry entegrasyon seçenekleri ve Grafana'da metrics/logs/traces yorumlama akışı aşağıdaki resmi dokümanlar temel alınarak hazırlandı.