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
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.
<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>
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
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]
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.
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();
}
}
`@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.
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);
}
}
}
`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.
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
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ı.
- Spring Boot 4 Observability Documentation — Micrometer Observation, OpenTelemetry support, OTLP export ve context propagation notları.
- OpenTelemetry Spring Boot Starter Documentation — Spring Boot uygulamalarında OTel starter ve Java agent seçenekleri.
- Grafana Metrics, Logs and Traces Overview — metrics, logs, traces ve observability sinyallerinin Grafana tarafında yorumlanması.