Logback & SLF4J Temelleri
Giriş — System.out.println Neden Yetersiz?
Hepimiz Java'ya ilk başladığımızda System.out.println() ile debugging yaptık. Hatta bazen production kodu arasına bile sıkıştırdık. Ama bir gün production'da bir bug çıktığında ve log dosyalarına baktığında sadece şunu görüyorsun:
siparis calisti
buraya girdi
degeri: 42
null geldi???Hangi kullanıcı? Hangi zaman? Hangi thread? Hangi sınıf? Hiçbir fikrin yok. İşte bu yüzden profesyonel logging hayati önem taşır.
System.out.println'in sorunları şunlar:
Zaman damgası yok — ne zaman olduğunu bilemezsin
Seviye yok — hata mı, bilgi mi, debug mi ayırt edemezsin
Filtreleme yok — production'da debug mesajlarını kapatamaz, sadece belirli paketleri izleyemezsin
Dosyaya yazamaz — konsola yazıyor, sunucu restart olduğunda her şey uçar
Performans — synchronized bir metot, yoğun trafikte darboğaz yaratır
Rotasyon yok — log dosyası sonsuza kadar büyür, diski patlatır
Production'da ihtiyacın olan şey: yapılandırılabilir, seviyeli, rotasyonlu, asenkron, takip edilebilir bir logging sistemi. İşte Logback ve SLF4J tam olarak bunu sağlıyor.
SLF4J — Facade Pattern ile Logging
Facade Pattern Nedir?
Bir binanın dış cephesini düşün. Binanın iç yapısını bilmeden, dışarıdan güzel bir arayüz görürsün. SLF4J (Simple Logging Facade for Java) tam olarak bu — bir facade (cephe).
Java dünyasında birçok logging kütüphanesi var:
Logback — Spring Boot'un varsayılanı
Log4j2 — Apache'nin popüler kütüphanesi
java.util.logging (JUL) — JDK ile gelen
Eğer doğrudan Logback API'sini kullanırsan ve bir gün Log4j2'ye geçmek istersen, tüm kodunu değiştirmen gerekir. Ama SLF4J kullanırsan, sadece dependency'yi değiştirirsin — kodun aynı kalır.
┌─────────────────────────┐
│ Senin Kodun │
│ log.info("Merhaba") │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ SLF4J API │ ← Facade (arayüz)
│ (slf4j-api.jar) │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ Logback / Log4j2 / │ ← Implementation (gerçek iş)
│ JUL / vs. │
└─────────────────────────┘Spring Boot, spring-boot-starter dependency'si içinde Logback + SLF4J kombinasyonunu zaten getiriyor. Ekstra bir şey eklemeye gerek yok.
Neden Doğrudan Logback Kullanmıyoruz?
// ❌ YANLIŞ — Doğrudan Logback'e bağımlısın
import ch.qos.logback.classic.Logger;
// ✅ DOĞRU — SLF4J facade kullan
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;Nedenler:
Taşınabilirlik: Yarın Log4j2'ye geçmek istersen kodun değişmez
Kütüphane uyumluluğu: Yazdığın library başka projede kullanıldığında, o projenin logging framework'ünü kullanır
Spring Boot uyumu: Spring Boot ve tüm Spring kütüphaneleri SLF4J kullanır
💡 İpucu: Spring Boot projelerinde logging için ekstra dependency eklemeye gerek yok.
spring-boot-starterzaten SLF4J + Logback getiriyor. Eğer Log4j2 kullanmak istersen, Logback'i exclude edip Log4j2 starter'ı eklemelisin.
Log Levels — Ne Zaman Hangisi?
SLF4J'de 5 log seviyesi var, en detaylıdan en kritiğe doğru:
TRACE → DEBUG → INFO → WARN → ERRORLog seviyesini INFO olarak ayarlarsan, sadece INFO, WARN, ERROR mesajları görünür. TRACE ve DEBUG filtrelenir.
Pratik Kurallar
| Seviye | Ne Zaman Kullanılır? | Örnek |
|---|---|---|
| TRACE | Metot giriş-çıkışları, adım adım akış. Çok detaylı, genelde kapalı | Entering calculateDiscount(), params: userId=42, cartTotal=150.0 |
| DEBUG | Geliştirme sırasında faydalı bilgi. Production'da genelde kapalı | Found 3 items in cart for userId=42 |
| INFO | Uygulamanın normal işleyişindeki önemli olaylar. Production'da açık | Order #1234 created for userId=42, total=150.0 TL |
| WARN | Potansiyel sorun var ama uygulama çalışmaya devam ediyor | Payment gateway response slow: 3500ms (threshold: 2000ms) |
| ERROR | Bir şeyler ciddi şekilde yanlış gitti. Müdahale gerekebilir | Failed to process payment for order #1234: Connection refused |
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order createOrder(Long userId, List<CartItem> items) {
log.trace("Entering createOrder() with userId={}, itemCount={}", userId, items.size());
log.debug("Calculating total for {} items", items.size());
BigDecimal total = calculateTotal(items);
log.info("Order created: userId={}, total={} TL", userId, total);
if (total.compareTo(new BigDecimal("10000")) > 0) {
log.warn("High value order detected: {} TL for userId={}", total, userId);
}
try {
paymentService.charge(userId, total);
} catch (PaymentException e) {
log.error("Payment failed for userId={}, amount={}: {}", userId, total, e.getMessage(), e);
throw e;
}
log.trace("Exiting createOrder()");
return order;
}
}⚠️ Dikkat:
log.error()çağırırken exception objesini son parametre olarak geç. SLF4J bunu otomatik olarak stack trace'e çevirir.e.getMessage()yeterli değil — full stack trace lazım!
// ❌ YANLIŞ — stack trace kaybolur
log.error("Payment failed: " + e.getMessage());
// ✅ DOĞRU — full stack trace loglanır
log.error("Payment failed for orderId={}: {}", orderId, e.getMessage(), e);Spring Boot Default Logging Konfigürasyonu
Spring Boot, kutudan çıktığı haliyle Logback ile gelir ve makul default ayarları vardır. Basit konfigürasyon için application.properties veya application.yml yeterlidir.
application.properties ile Temel Ayarlar
# Root log seviyesi (varsayılan: INFO)
logging.level.root=INFO
# Belirli paketler için seviye
logging.level.com.myapp=DEBUG
logging.level.com.myapp.repository=TRACE
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=DEBUG
# Log dosyasına yazma
logging.file.name=logs/application.log
# Veya sadece dizin belirt (dosya adı spring.log olur)
# logging.file.path=logs/
# Konsol pattern
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# Dosya pattern
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# Dosya boyutu limiti (varsayılan: 10MB)
logging.logback.rollingpolicy.max-file-size=10MB
# Toplam log boyutu limiti
logging.logback.rollingpolicy.total-size-cap=1GB
# Kaç gün saklanacak
logging.logback.rollingpolicy.max-history=30application.yml ile Aynı Ayarlar
logging:
level:
root: INFO
com.myapp: DEBUG
com.myapp.repository: TRACE
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
file:
name: logs/application.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
logback:
rollingpolicy:
max-file-size: 10MB
total-size-cap: 1GB
max-history: 30💡 İpucu:
logging.level.org.hibernate.SQL=DEBUGsadece SQL sorgularını gösterir. Parametre değerlerini de görmek istersen:logging.level.org.hibernate.orm.jdbc.bind=TRACEekle.
Bu basit konfigürasyon çoğu proje için yeterlidir. Ama daha fazla kontrol istersen — birden fazla dosyaya yazmak, asenkron loglama, özel pattern'ler — o zaman logback-spring.xml devreye girer.
logback-spring.xml — Detaylı Konfigürasyon
logback-spring.xml dosyasını src/main/resources/ altına koy. Spring Boot bunu otomatik olarak algılar ve application.properties'deki logging ayarlarının yerine kullanır.
⚠️ Dikkat:
logback.xmlyerine `logback-spring.xml` kullan.-springsoneki Spring Boot'un profile desteği ve özel etiketlerini (<springProfile>,<springProperty>) kullanmanı sağlar.
Temel Yapı
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Spring Boot default'larını dahil et (renklendirme, varsayılan pattern vs.) -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- Değişken tanımlama -->
<property name="LOG_PATH" value="logs"/>
<property name="APP_NAME" value="myapp"/>
<!-- Appender'lar buraya -->
<!-- Root logger -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>Console Appender — Renkli Konsol Çıktısı
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%blue(%thread)] %cyan(%logger{36}) - %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>Pattern elementleri:
| Element | Açıklama | Örnek Çıktı |
|---|---|---|
%d{pattern} | Tarih/saat | 2025-01-15 14:30:22.456 |
%-5level | Log seviyesi (5 karakter, sola hizalı) | INFO , ERROR |
%thread | Thread adı | http-nio-8080-exec-1 |
%logger{36} | Logger adı (max 36 karakter) | c.m.service.OrderService |
%msg | Log mesajı | Order #123 created |
%n | Yeni satır | |
%highlight() | Seviyeye göre renklendirme | ERROR=kırmızı, WARN=sarı |
%blue() | Mavi renk | |
%cyan() | Cyan renk | |
%X{key} | MDC değeri | %X{correlationId} |
File Appender — Dosyaya Yazma
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<append>true</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>Bu basit file appender, tek bir dosyaya yazar ve dosya sürekli büyür. Production için rolling file appender kullanmalısın.
Rolling File Appender — Akıllı Rotasyon
Diyelim ki uygulamanın günde 500MB log üretiyor. Bir hafta sonra 3.5GB, bir ay sonra 15GB... Disk dolar, uygulama çöker. Rolling file appender log dosyalarını otomatik olarak böler, sıkıştırır ve eski dosyaları temizler.
Boyut + Zaman Bazlı Rotasyon (Önerilen)
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- Günlük rotasyon, .gz ile sıkıştır -->
<fileNamePattern>
${LOG_PATH}/archived/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz
</fileNamePattern>
<!-- Tek dosya max 50MB -->
<maxFileSize>50MB</maxFileSize>
<!-- Son 30 günü sakla -->
<maxHistory>30</maxHistory>
<!-- Toplam max 5GB -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>Bu konfigürasyon ile:
Her gün yeni log dosyası oluşur:
myapp.2025-01-15.0.log.gzDosya 50MB'ı aşarsa gün içinde de bölünür:
myapp.2025-01-15.1.log.gz30 günden eski dosyalar otomatik silinir
Toplam boyut 5GB'ı aşarsa en eski dosyalar silinir
Error-Only Dosya (Ayrı Hata Logları)
Production'da sadece hataları görmek istediğin bir dosya da oluşturabilirsin:
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<!-- Sadece ERROR seviyesini yaz -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>Async Appender — Performans İçin Asenkron Loglama
Disk I/O yavaş bir işlemdir. Yoğun trafikte log.info() çağrısı thread'ini bloklar ve uygulamanın performansını düşürür. Async appender log mesajlarını bir kuyruğa atar ve ayrı bir thread ile dosyaya yazar.
<!-- Önce normal appender'ı tanımla -->
<appender name="FILE_SYNC" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Async wrapper -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- Kuyruk boyutu (varsayılan: 256) -->
<queueSize>1024</queueSize>
<!-- Kuyruk %80 dolduğunda DEBUG ve TRACE'i at (varsayılan: 20) -->
<discardingThreshold>20</discardingThreshold>
<!-- Caller bilgisi (sınıf, metot, satır) dahil et — performans maliyeti var -->
<includeCallerData>false</includeCallerData>
<!-- Uygulama kapanırken kuyruğun boşalmasını bekle (ms) -->
<maxFlushTime>5000</maxFlushTime>
<!-- Asla bloklanma — kuyruk dolunca mesajı at -->
<neverBlock>true</neverBlock>
<!-- Sarılacak appender -->
<appender-ref ref="FILE_SYNC"/>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
</root>⚠️ Dikkat: Async appender kullanırken
includeCallerDataözelliğinifalsebırak.trueyapılırsa her log satırı için stack trace çıkarılır ve performans ciddi şekilde düşer — async kullanmanın amacını yok eder.
💡 İpucu:
neverBlock=trueyoğun trafikte logların kaybolmasına sebep olabilir ama uygulamanın bloklanmasını önler. Trade-off: log kaybı mı, yoksa uygulama yavaşlaması mı? Production'da genelde log kaybı daha kabul edilebilir.
MDC — Request Tracking ve Correlation ID
Diyelim ki 100 kullanıcı aynı anda sisteme istek atıyor. Log dosyasında şöyle bir şey görüyorsun:
14:30:22 INFO OrderService - Creating order for userId=42
14:30:22 INFO OrderService - Creating order for userId=87
14:30:22 INFO PaymentService - Charging user
14:30:22 INFO PaymentService - Charging user
14:30:22 ERROR PaymentService - Payment failed!Hangi Payment failed! hangi Creating order'a ait? Belli değil. MDC (Mapped Diagnostic Context) her request'e benzersiz bir ID atar ve tüm log satırlarına bu ID'yi ekler.
Filter ile Otomatik MDC Ekleme
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MDCFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
// Benzersiz correlation ID oluştur
String correlationId = UUID.randomUUID().toString().substring(0, 8);
// Gelen header'dan al (microservice zinciri için)
HttpServletRequest httpRequest = (HttpServletRequest) request;
String incomingId = httpRequest.getHeader("X-Correlation-ID");
if (incomingId != null) {
correlationId = incomingId;
}
// MDC'ye koy
MDC.put("correlationId", correlationId);
MDC.put("userId", getCurrentUserId(httpRequest));
MDC.put("requestURI", httpRequest.getRequestURI());
// Response header'a da ekle (debugging için)
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("X-Correlation-ID", correlationId);
chain.doFilter(request, response);
} finally {
// ÇOK ÖNEMLİ: Thread pool'da thread tekrar kullanılır
// MDC temizlenmezse başka request'in bilgisi sızar!
MDC.clear();
}
}
private String getCurrentUserId(HttpServletRequest request) {
// Security context'ten al veya "anonymous" döndür
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return auth.getName();
}
return "anonymous";
}
}Pattern'e MDC Ekleme
<pattern>
%d{HH:mm:ss.SSS} %-5level [%thread] [%X{correlationId}] [%X{userId}] %logger{36} - %msg%n
</pattern>Artık loglar böyle görünüyor:
14:30:22.123 INFO [http-nio-8080-exec-1] [a3f2b8c1] [user42] OrderService - Creating order
14:30:22.125 INFO [http-nio-8080-exec-2] [7d9e4f12] [user87] OrderService - Creating order
14:30:22.340 INFO [http-nio-8080-exec-1] [a3f2b8c1] [user42] PaymentService - Charging user
14:30:22.342 INFO [http-nio-8080-exec-2] [7d9e4f12] [user87] PaymentService - Charging user
14:30:22.510 ERROR [http-nio-8080-exec-2] [7d9e4f12] [user87] PaymentService - Payment failed!Correlation ID 7d9e4f12 ile filtrelersen, user87'nin tüm akışını görebilirsin. Production debugging'de hayat kurtarır.
Async İşlemlerde MDC
@Async metotları farklı thread'de çalıştığı için MDC otomatik olarak taşınmaz. Bunu çözmek için:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
// MDC'yi async thread'e taşıyan dekoratör
executor.setTaskDecorator(new MDCTaskDecorator());
executor.initialize();
return executor;
}
}
public class MDCTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// Ana thread'deki MDC'yi yakala
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// Async thread'e MDC'yi aktar
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}⚠️ Dikkat: MDC
ThreadLocaltabanlıdır. Thread pool kullanan her yapıda (async, scheduled tasks, CompletableFuture) MDC bilgisini manuel olarak taşımalısın. Aksi halde log'larda yanlış correlation ID görürsün.
Profile-Specific Logging
Geliştirme ortamında her detayı görmek istersin. Production'da ise sadece önemli bilgileri loglamak istersin. Spring Boot profilleri ile bunu kolayca ayarlayabilirsin.
application.properties ile
# application-dev.properties
logging.level.root=DEBUG
logging.level.com.myapp=TRACE
logging.level.org.hibernate.SQL=DEBUG
# application-prod.properties
logging.level.root=WARN
logging.level.com.myapp=INFO
logging.level.org.hibernate.SQL=WARNlogback-spring.xml ile (Önerilen)
logback-spring.xml içinde <springProfile> etiketi kullanarak profile'a göre farklı konfigürasyon yapabilirsin:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<property name="LOG_PATH" value="logs"/>
<!-- Spring property'lerini Logback'te kullan -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name"
defaultValue="myapp"/>
<!-- Console appender — her ortamda kullanılır -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{HH:mm:ss.SSS} %highlight(%-5level) [%blue(%thread)] [%X{correlationId}] %cyan(%logger{36}) - %msg%n
</pattern>
</encoder>
</appender>
<!-- File appender — sadece prod'da kullanılır -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{correlationId}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- ERROR-only dosya -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{correlationId}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Async wrapper -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<discardingThreshold>20</discardingThreshold>
<neverBlock>true</neverBlock>
<appender-ref ref="FILE"/>
</appender>
<!-- ========== DEV Profili ========== -->
<springProfile name="dev">
<logger name="com.myapp" level="DEBUG"/>
<logger name="org.springframework.web" level="DEBUG"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="org.hibernate.orm.jdbc.bind" level="TRACE"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- ========== PROD Profili ========== -->
<springProfile name="prod">
<logger name="com.myapp" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- ========== Default (profil belirtilmezse) ========== -->
<springProfile name="default">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>Bu tam konfigürasyonla:
Dev'de: Konsola renkli, detaylı log (DEBUG + SQL sorguları)
Prod'da: Konsol + dosya + ayrı error dosyası, async yazma, rotasyon
Parameterized Logging — String Concat YAPMA
Bu küçük ama kritik bir konu. Yanlış yapılan loglama ciddi performans kaybına yol açar.
// ❌ YANLIŞ — Her seferinde string concatenation yapılır
// (log seviyesi DEBUG'ın altındaysa bile!)
log.debug("User " + username + " placed order " + orderId + " with total " + total);
// ❌ YANLIŞ — String.format da gereksiz maliyet
log.debug(String.format("User %s placed order %d with total %.2f", username, orderId, total));
// ✅ DOĞRU — Parameterized logging
// Log seviyesi DEBUG değilse, string oluşturma işlemi YAPILMAZ
log.debug("User {} placed order {} with total {}", username, orderId, total);Neden önemli? Diyelim production'da log seviyesi INFO. log.debug() çağrıları görmezden gelinir ama string concatenation yine de gerçekleşir. Yüksek trafikte bu gereksiz string oluşturmaları ciddi garbage collection baskısı yaratır.
SLF4J parameterized logging'de string sadece log seviyesi uygunsa oluşturulur. Sıfır maliyet.
Pahalı İşlemler İçin Guard Kullanımı
Bazen log mesajı için pahalı bir işlem gerekebilir:
// ❌ YANLIŞ — toString() her durumda çalışır
log.debug("Cart details: {}", cart.toDetailedString());
// ✅ DOĞRU — Önce seviye kontrolü yap
if (log.isDebugEnabled()) {
log.debug("Cart details: {}", cart.toDetailedString());
}💡 İpucu: Basit değişkenler için
isDebugEnabled()kontrolü gereksizdir. SLF4J parameterized logging zaten bunu optimize eder. Guard sadece parametre oluşturmanın kendisi pahalıysa (toDetailedString, collection dönüşümü vs.) gereklidir.
Logger Best Practices
Logger Tanımlama
// Yöntem 1: Manuel tanımlama
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
}
// Yöntem 2: Lombok @Slf4j (Önerilen)
@Slf4j
@Service
public class OrderService {
// 'log' değişkeni otomatik tanımlanır
// private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void process() {
log.info("Processing order...");
}
}Lombok @Slf4j kullanmanın avantajları:
Daha az boilerplate kod
Copy-paste hatası yok (sınıf adı otomatik alınır)
Tüm Spring Boot geliştirme ekipleri yaygın olarak kullanıyor
// ❌ Copy-paste hatası — sınıf adı YANLIŞ
@Service
public class PaymentService {
// OrderService'ten kopyalandı, sınıf adı değişmedi!
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
}💡 İpucu: Lombok kullanıyorsan (ki Spring Boot projelerinde neredeyse standart),
@Slf4jile devam et. Kullanmıyorsanprivate static final Logger log = LoggerFactory.getLogger(ClassName.class)standart kalıbını kullan. Değişken adınılogyap —logger,LOG,LOGGERkarmaşası olmasın.
Logger İsimlendirme Kuralları
Logger adı genellikle sınıf adıdır ve paket hiyerarşisini takip eder:
com.myapp → root logger'dan INFO alır
com.myapp.service → service alt paketi
com.myapp.service.OrderService → spesifik sınıflogback-spring.xml'de com.myapp.service seviyesini DEBUG yapınca, com.myapp.service.OrderService ve com.myapp.service.PaymentService da DEBUG olur. Hiyerarşik yapı bu.
Sensitive Data — LOGLAMA!
Bu konu çok kritik ve sık yapılan bir hata. KVKK, GDPR gibi regülasyonlar hassas veri loglamayı yasal sorun haline getiriyor.
Asla Loglanmaması Gereken Veriler
// ❌ YASAK — Şifre loglama
log.info("User login attempt: username={}, password={}", username, password);
// ❌ YASAK — Kredi kartı numarası
log.info("Payment processing: cardNumber={}", cardNumber);
// ❌ YASAK — TC kimlik numarası
log.info("User registered: tcNo={}", tcKimlikNo);
// ❌ YASAK — Token/secret
log.debug("API call with token: {}", apiToken);
// ✅ DOĞRU — Maskeleme
log.info("User login attempt: username={}", username);
log.info("Payment processing: cardNumber=****{}", cardNumber.substring(cardNumber.length() - 4));
log.info("User registered: tcNo=***masked***");Otomatik Maskeleme Utility
public class LogMaskUtil {
public static String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 4) return "****";
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
}
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) return "***@***";
String[] parts = email.split("@");
String name = parts[0];
String masked = name.charAt(0) + "***" + name.charAt(name.length() - 1);
return masked + "@" + parts[1];
}
public static String maskTcNo(String tcNo) {
if (tcNo == null || tcNo.length() < 3) return "***";
return "***" + tcNo.substring(tcNo.length() - 3);
}
}// Kullanım
log.info("Order placed: email={}, card={}",
LogMaskUtil.maskEmail(user.getEmail()), // t***n@gmail.com
LogMaskUtil.maskCardNumber(card.getNumber()) // ****-****-****-4242
);toString() Tuzağı
Entity sınıflarında toString() metodu (Lombok @ToString dahil) hassas alanları içerebilir:
// ❌ TEHLİKELİ — toString() tüm field'ları döndürür
@Data
@Entity
public class User {
private Long id;
private String username;
private String password; // toString() ile loglara sızar!
private String tcKimlikNo; // bu da!
}
// log.info("User: {}", user);
// Çıktı: User(id=1, username=john, password=abc123, tcKimlikNo=12345678901)
// ✅ GÜVENLİ — Hassas alanları exclude et
@Data
@ToString(exclude = {"password", "tcKimlikNo"})
@Entity
public class User {
private Long id;
private String username;
private String password;
private String tcKimlikNo;
}⚠️ Dikkat: Entity sınıflarında
@Dataveya@ToStringkullanıyorsan, hassas alanları mutlaka exclude et. Tek bir careless log statement ile KVKK/GDPR ihlali yapabilirsin. Bu sadece para cezası değil, güvenlik açığıdır.
Spring Boot Actuator ile Runtime Log Level Değiştirme
Production'da bir bug çıktığında, uygulamayı restart etmeden log seviyesini değiştirmek isteyebilirsin. Spring Boot Actuator bunu sağlar.
Kurulum
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency># application.properties
management.endpoints.web.exposure.include=loggers
management.endpoint.loggers.enabled=trueKullanım
# Tüm logger'ları listele
curl http://localhost:8080/actuator/loggers
# Belirli bir logger'ın seviyesini gör
curl http://localhost:8080/actuator/loggers/com.myapp.service.OrderService
# Yanıt:
# {"configuredLevel": "INFO", "effectiveLevel": "INFO"}
# Runtime'da seviyeyi değiştir
curl -X POST http://localhost:8080/actuator/loggers/com.myapp.service.OrderService \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
# Tüm com.myapp paketini DEBUG yap
curl -X POST http://localhost:8080/actuator/loggers/com.myapp \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
# Seviyeyi sıfırla (parent'tan miras alsın)
curl -X POST http://localhost:8080/actuator/loggers/com.myapp \
-H "Content-Type: application/json" \
-d '{"configuredLevel": null}'Bu, production'da uygulamayı restart etmeden debugging yapmanı sağlar. Bug'ı çözdükten sonra seviyeyi tekrar INFO'ya çekersin.
⚠️ Dikkat: Actuator endpoint'lerini production'da güvenli hale getir. Herkese açık bırakma! Spring Security ile koruma altına al veya sadece internal network'ten erişime izin ver.
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}Gerçek Dünya Örneği: E-Ticaret Uygulamasında Katmanlı Loglama
Bir e-ticaret uygulamasında sipariş akışını katman katman loglanmış haliyle görelim. Bu örnek, tüm öğrendiğimiz kavramları bir araya getirir.
Controller Katmanı
@Slf4j
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) {
log.info("POST /api/orders — userId={}, itemCount={}",
request.getUserId(), request.getItems().size());
long start = System.currentTimeMillis();
try {
OrderResponse response = orderService.createOrder(request);
long elapsed = System.currentTimeMillis() - start;
log.info("Order created successfully: orderId={}, total={} TL, elapsed={}ms",
response.getOrderId(), response.getTotal(), elapsed);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (InsufficientStockException e) {
log.warn("Order creation failed — insufficient stock: userId={}, productId={}",
request.getUserId(), e.getProductId());
throw e;
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - start;
log.error("Order creation failed: userId={}, elapsed={}ms",
request.getUserId(), elapsed, e);
throw e;
}
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long orderId) {
log.debug("GET /api/orders/{}", orderId);
OrderResponse response = orderService.getOrder(orderId);
return ResponseEntity.ok(response);
}
}Service Katmanı
@Slf4j
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ProductService productService;
private final PaymentService paymentService;
private final NotificationService notificationService;
// Constructor injection...
public OrderResponse createOrder(CreateOrderRequest request) {
log.debug("Starting order creation for userId={}", request.getUserId());
// 1. Stok kontrolü
log.debug("Checking stock for {} items", request.getItems().size());
for (OrderItemRequest item : request.getItems()) {
int available = productService.getAvailableStock(item.getProductId());
log.trace("Stock check: productId={}, requested={}, available={}",
item.getProductId(), item.getQuantity(), available);
if (available < item.getQuantity()) {
log.warn("Insufficient stock: productId={}, requested={}, available={}",
item.getProductId(), item.getQuantity(), available);
throw new InsufficientStockException(item.getProductId(), available);
}
}
// 2. Sipariş oluştur
Order order = buildOrder(request);
order = orderRepository.save(order);
log.info("Order persisted: orderId={}, status={}", order.getId(), order.getStatus());
// 3. Ödeme
try {
paymentService.processPayment(order);
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
log.info("Payment processed: orderId={}, amount={} TL",
order.getId(), order.getTotalAmount());
} catch (PaymentException e) {
order.setStatus(OrderStatus.PAYMENT_FAILED);
orderRepository.save(order);
log.error("Payment failed: orderId={}, amount={} TL, reason={}",
order.getId(), order.getTotalAmount(), e.getMessage(), e);
throw e;
}
// 4. Bildirim (async — hata olursa sipariş iptal edilmemeli)
try {
notificationService.sendOrderConfirmation(order);
log.debug("Order confirmation notification sent: orderId={}", order.getId());
} catch (Exception e) {
log.warn("Failed to send notification for orderId={}: {}",
order.getId(), e.getMessage());
// Bildirim hatası siparişi iptal etmemeli
}
return OrderResponse.from(order);
}
}Repository Katmanı
@Slf4j
@Repository
public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<Order> findOrdersByDateRange(LocalDate from, LocalDate to) {
log.debug("Querying orders: from={}, to={}", from, to);
long start = System.currentTimeMillis();
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o WHERE o.createdAt BETWEEN :from AND :to", Order.class)
.setParameter("from", from.atStartOfDay())
.setParameter("to", to.plusDays(1).atStartOfDay())
.getResultList();
long elapsed = System.currentTimeMillis() - start;
log.debug("Query completed: {} orders found, elapsed={}ms", orders.size(), elapsed);
if (elapsed > 1000) {
log.warn("Slow query detected: findOrdersByDateRange took {}ms " +
"(from={}, to={}, resultCount={})", elapsed, from, to, orders.size());
}
return orders;
}
}Tamamlanmış Log Çıktısı
Tüm bu katmanlar bir araya geldiğinde, production'da tek bir isteğin logu şöyle görünür:
14:30:22.001 INFO [exec-1] [a3f2b8c1] [user42] OrderController - POST /api/orders — userId=42, itemCount=2
14:30:22.005 DEBUG [exec-1] [a3f2b8c1] [user42] OrderService - Starting order creation for userId=42
14:30:22.008 DEBUG [exec-1] [a3f2b8c1] [user42] OrderService - Checking stock for 2 items
14:30:22.015 TRACE [exec-1] [a3f2b8c1] [user42] OrderService - Stock check: productId=101, requested=2, available=15
14:30:22.018 TRACE [exec-1] [a3f2b8c1] [user42] OrderService - Stock check: productId=205, requested=1, available=8
14:30:22.045 INFO [exec-1] [a3f2b8c1] [user42] OrderService - Order persisted: orderId=1234, status=CREATED
14:30:22.320 INFO [exec-1] [a3f2b8c1] [user42] OrderService - Payment processed: orderId=1234, amount=299.90 TL
14:30:22.325 DEBUG [exec-1] [a3f2b8c1] [user42] OrderService - Order confirmation notification sent: orderId=1234
14:30:22.328 INFO [exec-1] [a3f2b8c1] [user42] OrderController - Order created successfully: orderId=1234, total=299.90 TL, elapsed=327msCorrelation ID a3f2b8c1 ile tüm akışı takip edebiliyorsun. Hangi adımda ne kadar zaman harcandığını, hangi ürünlerin kontrol edildiğini, ödemenin ne zaman tamamlandığını — her şeyi.
Tam logback-spring.xml — Production-Ready Konfigürasyon
Tüm öğrendiklerimizi birleştiren, production'a hazır bir konfigürasyon:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"
defaultValue="ecommerce-app"/>
<property name="LOG_PATH" value="${LOG_PATH:-logs}"/>
<property name="CONSOLE_PATTERN"
value="%d{HH:mm:ss.SSS} %highlight(%-5level) [%blue(%15.15thread)] [%yellow(%8.8X{correlationId})] %cyan(%-40.40logger{39}) : %msg%n"/>
<property name="FILE_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%15.15thread] [%8X{correlationId}] [%X{userId}] %-40.40logger{39} : %msg%n"/>
<!-- Console -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- Application log -->
<appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder><pattern>${FILE_PATTERN}</pattern></encoder>
</appender>
<!-- Error-only log -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder><pattern>${FILE_PATTERN}</pattern></encoder>
</appender>
<!-- Async wrappers -->
<appender name="ASYNC_APP" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<discardingThreshold>20</discardingThreshold>
<neverBlock>true</neverBlock>
<maxFlushTime>5000</maxFlushTime>
<appender-ref ref="APP_FILE"/>
</appender>
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>256</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>false</neverBlock>
<maxFlushTime>10000</maxFlushTime>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- ========== DEV ========== -->
<springProfile name="dev">
<logger name="com.myapp" level="DEBUG"/>
<logger name="org.springframework.web" level="DEBUG"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="org.hibernate.orm.jdbc.bind" level="TRACE"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- ========== STAGING ========== -->
<springProfile name="staging">
<logger name="com.myapp" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_APP"/>
<appender-ref ref="ASYNC_ERROR"/>
</root>
</springProfile>
<!-- ========== PROD ========== -->
<springProfile name="prod">
<logger name="com.myapp" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_APP"/>
<appender-ref ref="ASYNC_ERROR"/>
</root>
</springProfile>
</configuration>scan="true" scanPeriod="30 seconds" ayarı, XML dosyasını her 30 saniyede bir kontrol eder ve değişiklik varsa otomatik yeniden yükler. Uygulamayı restart etmeden konfigürasyon güncelleyebilirsin (Actuator'a alternatif).
Özet
`System.out.println` kullanma — profesyonel projeler SLF4J + Logback kullanır. Zaman damgası, seviye, filtreleme, rotasyon ve performans açısından karşılaştırma bile yapılamaz
SLF4J facade pattern kullanır — kodun logging implementasyonundan bağımsız kalır.
import org.slf4j.Loggerher zaman doğru tercihParameterized logging yap (
log.info("User {} logged in", username)), string concatenation yapma (log.info("User " + username + " logged in")) — performans farkı yüksek trafikte dramatikMDC ile request tracking yap — correlationId olmadan production'da debugging kabus. Her request'e benzersiz ID ata, tüm katmanlarda logla
Hassas veri LOGLAMA — şifre, kredi kartı, TC no, token... KVKK/GDPR ihlali, güvenlik açığı.
@ToString(exclude = ...)kullanProfile-specific logging ayarla — dev'de DEBUG ile her şeyi gör, prod'da INFO ile sadece önemli olayları logla. Rolling file + async appender ile production-ready ol
AI Asistan
Sorularını yanıtlamaya hazır