Logging: SLF4J ve Logback
Uygulamanı yazdın, testler geçiyor, production'a deploy ettin. Sonra bir gece saat 3'te müşteriden mesaj geliyor: "Siparişim kayboldu." Kodu açıyorsun, bakıyorsun — hiçbir iz yok. Ne zaman hata oldu, hangi kullanıcı etkilendi, hangi adımda patladı... Hiçbir şey bilmiyorsun. Çünkü uygulamanda logging yok.
Logging, yazılımın kara kutusudur. Uçaklardaki flight recorder'ı düşün — uçuş boyunca her şeyi kaydeder. Pilotlar her iniş-kalkışta onu kontrol etmez, ama bir sorun olduğunda ilk baktıkları yer orasıdır. İyi bir logging altyapısı da tam bunu yapar: uygulaman çalışırken sessizce kaydeder, sorun olduğunda sana "şu kullanıcı, şu zamanda, şu endpoint'te, şu hatayı aldı" der.
Bu derste Java'nın standart logging çözümünü öğreneceksin: SLF4J (façade) ve Logback (implementasyon).
System.out.println Neden Yetmez?
Hepimiz oradan başladık. Bir şeyi debug etmek istediğimizde ilk refleksimiz System.out.println("buraya geldi") yazmaktır. Küçük bir Main sınıfında bu işe yarar. Ama gerçek bir uygulamada birkaç kritik sorun çıkar.
Seviye kontrolü yok. Diyelim ki debug için yüzlerce println ekledin. Uygulama düzgün çalışmaya başlayınca bunları tek tek silmen gerekiyor. Ya da hepsini if (DEBUG) ile sarmalamalısın. Her seferinde elle yönetmek kabusa döner.
Çıktı hedefi sabit. println sadece konsola yazar. Production'da log dosyasına yazmak istersen ne yapacaksın? Dosya I/O kodunu kendin mi yazacaksın? Dosya dolunca eski logları silme mantığını kendin mi kuracaksın?
Performans sorunu. "User " + userId + " purchased " + itemCount + " items" gibi string concatenation her çağrıda yapılır — o bilgiyi görmek istemesen bile. Saniyede binlerce istek alan bir serviste bu ciddi yük oluşturur.
Zaman damgası, thread bilgisi, sınıf adı yok. Konsolda sadece mesajı görürsün. Hangi thread'den geldi? Saat kaçta? Hangi sınıftan? Bunları elle eklemek her satırı üç kat uzatır.
Format tutarsızlığı. Ekipteki herkes farklı format kullanır — biri [ERROR], biri !!!HATA!!! yazar. Log dosyasını parse etmek imkânsız hale gelir.
Kısacası println, debugging için hızlı bir çözümdür ama production logging için tasarlanmamıştır. Bunun için profesyonel araçlar var.
SLF4J: Façade Pattern
Java dünyasında birden fazla logging kütüphanesi var: Logback, Log4j 2, java.util.logging (JUL). Her birinin API'si farklı. Eğer kodunu doğrudan Logback'e bağlarsan ve bir gün Log4j 2'ye geçmek istersen, yüzlerce import ve method çağrısını değiştirmen gerekir.
SLF4J (Simple Logging Facade for Java) bu sorunu çözer. Adındaki "Facade" kelimesi tasarım kalıbından gelir: SLF4J bir arayüz sağlar, arka plandaki implementasyonu sen seçersin.
Bunu şöyle düşün: evindeki elektrik prizi bir façade'dir. Prizin arkasındaki enerji kaynağı değişebilir — güneş, rüzgar, doğalgaz — ama sen hep aynı prize takarsın. SLF4J de böyle çalışır: kodun hep aynı API'yi kullanır, arka planda ne çalışıyor bilmez.
Senin Kodun
│
▼
SLF4J API ← Façade (arayüz)
│
▼
Logback ← İmplementasyon (değiştirilebilir)Pratikte standart kombinasyon SLF4J + Logback'tir. Logback zaten SLF4J'nin yaratıcısı (Ceki Gülcü) tarafından yazılmıştır — ikisi doğal uyum içinde çalışır.
Maven Dependency
Projenin pom.xml'ine iki dependency eklemen yeterli:
<dependencies>
<!-- SLF4J API — kodunda bunu kullanırsın -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<!-- Logback Classic — SLF4J implementasyonu -->
<!-- Bu dependency slf4j-api'yi transitive olarak da çeker,
ama explicit yazmak best practice'tir -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.16</version>
</dependency>
</dependencies>logback-classic eklediğinde, SLF4J otomatik olarak Logback'i keşfeder — classpath'te hangi implementasyonu bulursa onu kullanır (service provider mekanizması).
Logger Oluşturma
Her sınıfta bir Logger instance'ı oluşturursun. Kural basit: sınıf başına bir Logger, sınıfın kendi adıyla.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
// Standart pattern: private static final
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void placeOrder(String userId, String productId) {
logger.info("Placing order for user={}, product={}", userId, productId);
// İş mantığı...
logger.debug("Inventory check passed for product={}", productId);
}
}Neden `OrderService.class`? Log çıktısında hangi sınıftan geldiğini görmek için. Logback çıktıda c.m.s.OrderService gibi kısaltılmış ad gösterir — binlerce satır arasında kaynağı bulmayı kolaylaştırır.
Neden `static final`? Her object için yeni Logger yaratmaya gerek yok — sınıf seviyesinde tek instance yeterli. final çünkü değişmemeli.
Lombok kullanıyorsan @Slf4j annotation'ı ile bu satırı otomatik oluşturabilirsin — Lombok log adında bir field üretir. Ama Lombok kullanmasan bile o tek satır (private static final Logger...) yazılması gereken standart bir pattern. Ezberle, her sınıfta yaz.
Log Seviyeleri
Her log mesajının bir önem seviyesi vardır. SLF4J beş seviye tanımlar, en düşükten en yükseğe:
| Seviye | Ne Zaman Kullanılır | Örnek |
|---|---|---|
| TRACE | En detaylı, adım adım takip | Method giriş-çıkışları, döngü iterasyonları |
| DEBUG | Geliştirme sırasında faydalı detaylar | Sorgu parametreleri, hesaplama ara sonuçları |
| INFO | Normal iş akışının önemli adımları | Uygulama başladı, sipariş oluşturuldu, ödeme alındı |
| WARN | Potansiyel sorun, ama uygulama çalışmaya devam ediyor | Retry yapıldı, cache miss, deprecated API kullanımı |
| ERROR | Bir şeyler ters gitti, müdahale gerekebilir | Veritabanı bağlantı hatası, ödeme başarısız, dosya bulunamadı |
Seviyeler hiyerarşiktir. Seviyeyi INFO ayarlarsan, INFO ve üstü (WARN, ERROR) loglanır; DEBUG ve TRACE görmezden gelinir. Production'da genellikle INFO kullanılır — sorun çıktığında konfigürasyonu DEBUG'a çekip detay alırsın.
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
public boolean processPayment(String orderId, double amount) {
logger.trace("Entering processPayment() with orderId={}, amount={}", orderId, amount);
logger.debug("Validating payment amount: {}", amount);
if (amount <= 0) {
logger.warn("Invalid payment amount: {} for order={}", amount, orderId);
return false;
}
try {
// Ödeme gateway çağrısı
gateway.charge(orderId, amount);
logger.info("Payment successful: order={}, amount={}", orderId, amount);
return true;
} catch (PaymentException e) {
logger.error("Payment failed for order={}, amount={}", orderId, amount, e);
return false;
}
}
}Dikkat et: logger.error() çağrısında üçüncü parametre olarak exception nesnesini geçtik. SLF4J bunu özel olarak tanır ve stack trace'i otomatik olarak log çıktısına ekler. Bu çok önemli — hatanın nerede oluştuğunu görmeni sağlar.
Seviye Seçimi Rehberi
Yeni başlayanların en çok zorlandığı konu hangisini ne zaman kullanacaklarıdır. Basit bir kural:
Bir şey oldu ve herkese söylenmeli → INFO
Bir şey oldu ve sadece geliştirici ilgilenir → DEBUG
Bir şey olmadı ama olabilirdi → WARN
Bir şey patladı → ERROR
Adım adım her şeyi görmek istiyorum → TRACE
TRACE'i production'da neredeyse hiç açmazsın. O kadar detaylıdır ki log dosyan dakikalar içinde gigabyte'lara ulaşır.
Parameterized Logging
SLF4J'nin en güçlü özelliklerinden biri parameterized logging'dir. Placeholder olarak {} kullanırsın, değerler sırayla yerleştirilir:
// ✅ Doğru yol: Parameterized logging
logger.info("User {} logged in from IP {}", username, ipAddress);
// ❌ Yanlış yol: String concatenation
logger.info("User " + username + " logged in from IP " + ipAddress);İkisi de aynı çıktıyı üretir. Peki fark ne?
Performans. String concatenation'da, log seviyesi kapalı olsa bile string birleştirme her zaman yapılır. Yani DEBUG seviyesi kapalıyken bile şu satır:
logger.debug("Processing items: " + items.toString() + " for user: " + userId);...üç string concatenation ve bir toString() çağrısı yapar. Sonuç? O string hiçbir yere yazılmaz, ama CPU ve bellek harcandı. Saniyede on binlerce istek alan bir sistemde bu fark ciddi.
Parameterized logging'de ise SLF4J önce seviye kontrolü yapar. Seviye kapalıysa placeholder'ları hiç işlemez, string birleştirme olmaz. Sıfır maliyet.
Eğer parametrenin kendisi pahalı bir hesaplama gerektiriyorsa (mesela büyük bir listenin toString'i), Java 8+ ile birlikte guard pattern kullanabilirsin:
// Pahalı toString() çağrısından korunma
if (logger.isDebugEnabled()) {
logger.debug("Full item list: {}", expensiveList.toString());
}Ama normal string ve primitive değerler için guard pattern'a gerek yok — parameterized logging yeterli.
⚠️ Dikkat: Placeholder her zaman {} olmalıdır. printf-style %s veya %d çalışmaz. SLF4J'nin kendi format sistemi {}'dir, bunu karıştırma.
Logback Konfigürasyonu
Logback konfigürasyonu logback.xml dosyasıyla yapılır. Bu dosya src/main/resources/ dizinine konur — classpath'in köküne.
Üç temel kavram var:
Appender: Log mesajını nereye yazacağını belirler (konsol, dosya, network...)
Pattern Layout: Log mesajının formatını belirler (tarih, seviye, sınıf adı, mesaj...)
Logger / Root: Hangi seviyede, hangi appender'a yazacağını belirler
Temel Konfigürasyon
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Konsola yazan appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Dosyaya yazan appender -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/application.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Root logger: INFO seviyesi, hem konsola hem dosyaya yaz -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- Belirli bir package için farklı seviye -->
<logger name="com.myapp.repository" level="DEBUG" />
</configuration>Bu konfigürasyonla çıktı şöyle görünür:
2026-02-23 14:35:22.481 [main] INFO c.m.s.OrderService - Placing order for user=u123, product=p456
2026-02-23 14:35:22.512 [main] DEBUG c.m.r.OrderRepository - Executing SQL: INSERT INTO orders...
2026-02-23 14:35:22.598 [main] ERROR c.m.s.PaymentService - Payment failed for order=ORD-789Pattern Açıklaması
Pattern string'indeki her token bir anlam taşır:
| Token | Açıklama | Örnek Çıktı |
|---|---|---|
%d{format} | Tarih/saat | 2026-02-23 14:35:22.481 |
%thread | Thread adı | main, http-nio-8080-exec-1 |
%-5level | Seviye (5 karakter, sola hizalı) | INFO , ERROR |
%logger{36} | Logger adı (max 36 karakter, kısaltılır) | c.m.s.OrderService |
%msg | Log mesajı | Placing order for user=u123 |
%n | Satır sonu | (platform bağımsız newline) |
%X{key} | MDC değeri | (sonraki bölümde) |
%logger{36} — Logback package adlarını akıllıca kısaltır: com.myapp.service.OrderService → c.m.s.OrderService.
Package-Specific Seviye
Root logger tüm uygulama için geçerli seviyeyi belirler. Ama belirli package'lar için farklı seviye tanımlayabilirsin:
<!-- Tüm uygulama INFO seviyesinde -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<!-- Sadece repository katmanı DEBUG -->
<logger name="com.myapp.repository" level="DEBUG" />
<!-- Spring framework loglarını sustur -->
<logger name="org.springframework" level="WARN" />
<!-- Hibernate SQL loglarını göster (geliştirmede faydalı) -->
<logger name="org.hibernate.SQL" level="DEBUG" />Production'da çok değerli: sorun olduğunda sadece ilgili package'ın seviyesini DEBUG'a çekersin — tüm uygulamanın hacmini artırmadan hedefli debug.
Log Rotation: RollingFileAppender
Tek dosyaya sonsuza kadar yazmak iyi fikir değil — dosya büyür, disk dolar, grep yavaşlar. RollingFileAppender belirli koşullarda dosyayı "rotate" eder: mevcut dosyayı kapatıp yenisini açar.
Boyut Bazlı Rotation
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- Dosya adı pattern'ı: tarih + index -->
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<!-- Her dosya max 100MB -->
<maxFileSize>100MB</maxFileSize>
<!-- Son 30 günün loglarını tut -->
<maxHistory>30</maxHistory>
<!-- Toplam log boyutu max 3GB -->
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>Bu konfigürasyonla:
Her gün yeni bir dosya açılır (
application.2026-02-23.0.log.gz)Dosya 100MB'ı geçerse aynı gün içinde yeni bir dosya açılır (index artar:
.1.log.gz,.2.log.gz)Eski dosyalar
.gzile sıkıştırılır (disk tasarrufu)30 günden eski loglar silinir
Toplam 3GB'ı geçerse en eski dosyalar silinir
.gz uzantısı otomatik GZIP sıkıştırmayı aktive eder — text loglar genellikle %90+ sıkışır.
Error loglarını ayrı bir dosyada tutmak da yaygın bir pratiktir — ThresholdFilter ile sadece ERROR seviyesini kabul eden ikinci bir RollingFileAppender tanımlarsın. Böylece sorun araştırırken dev bir log dosyasında grep yapmak yerine doğrudan error.log'a bakarsın. Bu pattern'ı aşağıdaki production şablonunda göreceksin.
MDC: Mapped Diagnostic Context
Web uygulamasında onlarca thread aynı anda çalışır. Log dosyasına baktığında satırlar iç içe geçmiş olur:
14:35:22.481 [http-exec-1] INFO OrderService - Placing order
14:35:22.482 [http-exec-3] INFO OrderService - Placing order
14:35:22.485 [http-exec-1] DEBUG PaymentService - Charging card
14:35:22.486 [http-exec-3] ERROR PaymentService - Payment failed
14:35:22.490 [http-exec-1] INFO OrderService - Order completedHangi satır hangi request'e ait? MDC (Mapped Diagnostic Context) bu sorunu çözer. Thread-local bir Map'tir — her thread'e özgü key-value çiftleri koyarsın, log pattern'ında otomatik görünür.
import org.slf4j.MDC;
public class RequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
// Her request'e benzersiz bir ID ata
String requestId = UUID.randomUUID().toString().substring(0, 8);
String userId = extractUserId(request);
MDC.put("requestId", requestId);
MDC.put("userId", userId);
chain.doFilter(request, response);
} finally {
// ÖNEMLİ: Thread pool kullanılıyor, temizlemeyi unutma!
MDC.clear();
}
}
}Logback pattern'ına MDC değerlerini ekle:
<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{requestId}] [%X{userId}] %-5level %logger{36} - %msg%n</pattern>Şimdi çıktı böyle görünür:
14:35:22.481 [http-exec-1] [a3f8b2c1] [user-42] INFO OrderService - Placing order
14:35:22.482 [http-exec-3] [7e2d9f04] [user-88] INFO OrderService - Placing order
14:35:22.485 [http-exec-1] [a3f8b2c1] [user-42] DEBUG PaymentService - Charging card
14:35:22.486 [http-exec-3] [7e2d9f04] [user-88] ERROR PaymentService - Payment failedArtık grep "a3f8b2c1" application.log ile tek bir request'in tüm yolculuğunu görebilirsin. Microservice mimarisinde bu requestId'yi HTTP header olarak diğer servislere iletirsin — buna distributed tracing denir.
💡 İpucu: Spring Boot kullanıyorsan, Spring 3.x ile birlikte gelen Micrometer Tracing (eski adıyla Spring Cloud Sleuth) otomatik olarak traceId ve spanId oluşturur ve MDC'ye koyar. Elle filter yazmana gerek kalmaz.
⚠️ Dikkat: MDC thread-local çalışır. Thread pool kullanan ortamlarda (ki tüm web sunucuları öyle) MDC.clear() çağrısını finally bloğunda mutlaka yap. Yoksa bir önceki request'in MDC değerleri sonraki request'te görünür — hem güvenlik açığı hem debug kabusu.
Gerçek Dünya Örneği: E-Ticaret Sipariş Servisi
Şimdi öğrendiğimiz her şeyi bir araya getirelim. Bir e-ticaret uygulamasının sipariş servisini logging ile donatalım:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderService(InventoryService inventoryService,
PaymentService paymentService,
NotificationService notificationService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
public OrderResult createOrder(String userId, List<OrderItem> items) {
String orderId = generateOrderId();
// MDC ile tüm alt çağrılarda orderId görünsün
MDC.put("orderId", orderId);
MDC.put("userId", userId);
try {
logger.info("Creating order with {} items, total={}",
items.size(), calculateTotal(items));
// 1. Stok kontrolü
logger.debug("Checking inventory for {} items", items.size());
for (OrderItem item : items) {
if (!inventoryService.isAvailable(item.getProductId(), item.getQuantity())) {
logger.warn("Insufficient stock for product={}, requested={}",
item.getProductId(), item.getQuantity());
return OrderResult.outOfStock(item.getProductId());
}
}
// 2. Ödeme
double total = calculateTotal(items);
logger.debug("Initiating payment: amount={}", total);
try {
paymentService.charge(userId, total);
logger.info("Payment successful: amount={}", total);
} catch (PaymentException e) {
logger.error("Payment failed: amount={}", total, e);
return OrderResult.paymentFailed();
}
// 3. Stok düşürme
for (OrderItem item : items) {
inventoryService.reserve(item.getProductId(), item.getQuantity());
}
logger.debug("Inventory reserved for all items");
// 4. Bildirim
try {
notificationService.sendOrderConfirmation(userId, orderId);
logger.debug("Order confirmation notification sent");
} catch (NotificationException e) {
// Bildirim hatası siparişi iptal etmemeli — sadece logla
logger.warn("Failed to send order confirmation notification", e);
}
logger.info("Order created successfully");
return OrderResult.success(orderId);
} finally {
MDC.remove("orderId");
MDC.remove("userId");
}
}
private double calculateTotal(List<OrderItem> items) {
return items.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
}
private String generateOrderId() {
return "ORD-" + System.currentTimeMillis();
}
}Bu kodu logback.xml ile birlikte çalıştırdığında, bir sipariş akışının tüm çıktısı şöyle görünür:
14:35:22.100 [http-exec-1] [ORD-1740312922100] [user-42] INFO c.m.s.OrderService - Creating order with 3 items, total=259.97
14:35:22.105 [http-exec-1] [ORD-1740312922100] [user-42] DEBUG c.m.s.OrderService - Checking inventory for 3 items
14:35:22.120 [http-exec-1] [ORD-1740312922100] [user-42] DEBUG c.m.s.OrderService - Initiating payment: amount=259.97
14:35:22.350 [http-exec-1] [ORD-1740312922100] [user-42] INFO c.m.s.OrderService - Payment successful: amount=259.97
14:35:22.380 [http-exec-1] [ORD-1740312922100] [user-42] DEBUG c.m.s.OrderService - Inventory reserved for all items
14:35:22.410 [http-exec-1] [ORD-1740312922100] [user-42] DEBUG c.m.s.OrderService - Order confirmation notification sent
14:35:22.412 [http-exec-1] [ORD-1740312922100] [user-42] INFO c.m.s.OrderService - Order created successfullyHer satırda orderId ve userId var. Sorun çıktığında grep "ORD-1740312922100" ile tüm akışı görebilirsin.
Dikkat et: INFO sadece önemli iş olayları için, DEBUG ara adımlar için, WARN ciddi olmayan ama not edilmesi gereken durumlar için, ERROR sadece gerçek hatalar için kullanıldı. MDC ile orderId/userId her satırda otomatik görünüyor ve finally'de temizleniyor.
Best Practices: Ne Loglanmalı, Ne Loglanmamalı
Logla ✅
İş akışı olayları: Sipariş oluşturuldu, ödeme alındı, kullanıcı giriş yaptı
Hata ve exception'lar: Stack trace ile birlikte
Performans metrikleri: İşlem süresi, queue boyutu, bağlantı sayısı
Konfigürasyon değişiklikleri: Uygulama başlangıcında aktif profil, versiyon, önemli ayarlar
Retry ve fallback durumları: Tekrar denendi, alternatif yola geçildi
Dış servis çağrıları: Hangi servise gidildi, ne kadar sürdü, başarılı mı
Asla Loglama ❌
Şifreler, token'lar, API key'ler:
logger.info("User password: {}", password)— asla!Kredi kartı numaraları: PCI-DSS ihlali olur
Kişisel veriler (PII): TC kimlik, adres, telefon numarası dikkatle değerlendirilmeli
Session ID, JWT token: Ele geçirilirse hesap çalınır
Büyük veri yapıları: 10.000 elemanlı bir listeyi loglama — dosya şişer, performans düşer
// ❌ ASLA
logger.info("User login: email={}, password={}", email, password);
logger.debug("Credit card: {}", cardNumber);
logger.info("Token: {}", jwtToken);
// ✅ DOĞRU
logger.info("User login: email={}", email);
logger.debug("Credit card ending: {}", cardNumber.substring(cardNumber.length() - 4));
logger.info("Token issued for user={}", userId);GDPR ve KVKK gibi düzenlemeler log dosyalarını da kapsar. Loglarına PII yazarsan ve dosyalar sızarsa — veri ihlali bildirimi yapman gerekir.
Diğer İpuçları
Exception'ı doğru logla. logger.error("Failed: " + e.getMessage()) yerine logger.error("Failed", e) yaz — SLF4J exception nesnesini son parametre olarak alır ve stack trace'i otomatik ekler. Sadece mesajı loglarsan hatanın kaynağını asla bulamazsın.
Log mesajını anlamlı yaz. logger.info("Done") yerine logger.info("Order completed: orderId={}, duration={}ms", orderId, elapsed) yaz. Gece 3'te log dosyasına bakan kişi (muhtemelen sen) bağlamı hemen anlamalı.
Seviye seçiminde tutarlı ol. Ekip olarak bir convention belirleyin. Code review'da log seviyeleri de gözden geçirilmeli.
Production İpuçları
Yukarıdaki örnekleri birleştirirken production'da işine yarayacak birkaç ek özellik:
`scan="true"`: <configuration scan="true" scanPeriod="30 seconds"> ile Logback konfigürasyon dosyasını periyodik kontrol eder. Dosyayı değiştirirsen uygulama yeniden başlatılmadan yeni konfigürasyon yüklenir. Production'da seviye değişikliği için çok değerli.
`AsyncAppender`: Log yazma I/O gerektirdiği için yavaş olabilir. AsyncAppender log mesajlarını bir kuyruğa atar, ayrı bir thread dosyaya yazar. Ana thread bloklanmaz:
<appender name="ASYNC_APP" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold> <!-- 0 = hiçbir mesajı atma -->
<appender-ref ref="APP_FILE" />
</appender>`%X{requestId:-}`: MDC pattern'ında :- ifadesi, MDC'de requestId yoksa boş string yerine - gösterir. Log parse ederken tutarlılık sağlar.
Property kullanımı: Tekrarlanan pattern'ları <property> ile tanımla, ${LOG_PATTERN} şeklinde referans ver — DRY prensibi konfigürasyonda da geçerli.
Sık Yapılan Hatalar
1. Logger'ı yanlış sınıfla oluşturmak: Copy-paste'in klasik tuzağı. LoggerFactory.getLogger(PaymentService.class) yazıp OrderService'e koyarsın — log dosyasında yanlış sınıf adı görünür, debug sırasında saatlerce yanlış yere bakarsın.
2. Exception'ı yutmak: logger.error("Failed: " + e.getMessage()) sadece mesajı verir. Stack trace kayıp. Doğrusu: logger.error("Failed", e) — exception nesnesini son parametre olarak geç.
3. Döngüde aşırı loglama: Milyon iterasyonlu döngüde her adımı loglamak dosyayı patlatır. Çözüm: her N iterasyonda bir logla (if (i % 10_000 == 0)) veya döngü sonunda özet yaz.
4. Logla ama hatayı yut: logger.error("User not found") yazıp return null yapmak — çağıran kod sorun olmadığını sanır ve NullPointerException alır. Logla ve exception fırlat (veya Optional dön).
Özet
System.out.println debug için hızlı bir çözümdür ama production'da seviye kontrolü, dosyaya yazma ve format tutarlılığı sağlamaz — bunun için SLF4J + Logback kullan.
SLF4J bir façade'dir: kodun SLF4J API'sini kullanır, arka plandaki logging implementasyonu (Logback, Log4j 2) değiştirilebilir — koduna dokunmadan.
Log seviyeleri (TRACE → DEBUG → INFO → WARN → ERROR) mesajların önemini belirler. Production'da genellikle INFO seviyesi kullanılır; sorun anında hedefli olarak DEBUG'a çekilir.
Parameterized logging (
log.info("User {} logged in", username)) string concatenation'a göre hem daha okunabilir hem de seviye kapalıyken sıfır maliyetlidir.MDC (Mapped Diagnostic Context) her request'e benzersiz bir ID atayarak log dosyasında tek bir isteğin tüm yolculuğunu takip etmeni sağlar —
finally'de temizlemeyi unutma.Ne logladığına dikkat et: İş akışı olaylarını, hataları ve performans metriklerini logla; şifreleri, token'ları ve kişisel verileri asla loglama.
AI Asistan
Sorularını yanıtlamaya hazır