← Kursa Dön
📄 Text · 20 min

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:

  1. Zaman damgası yok — ne zaman olduğunu bilemezsin

  2. Seviye yok — hata mı, bilgi mi, debug mi ayırt edemezsin

  3. Filtreleme yok — production'da debug mesajlarını kapatamaz, sadece belirli paketleri izleyemezsin

  4. Dosyaya yazamaz — konsola yazıyor, sunucu restart olduğunda her şey uçar

  5. Performans — synchronized bir metot, yoğun trafikte darboğaz yaratır

  6. 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-starter zaten 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 → ERROR

Log seviyesini INFO olarak ayarlarsan, sadece INFO, WARN, ERROR mesajları görünür. TRACE ve DEBUG filtrelenir.

Pratik Kurallar

SeviyeNe Zaman Kullanılır?Örnek
TRACEMetot giriş-çıkışları, adım adım akış. Çok detaylı, genelde kapalıEntering calculateDiscount(), params: userId=42, cartTotal=150.0
DEBUGGeliştirme sırasında faydalı bilgi. Production'da genelde kapalıFound 3 items in cart for userId=42
INFOUygulamanın normal işleyişindeki önemli olaylar. Production'da açıkOrder #1234 created for userId=42, total=150.0 TL
WARNPotansiyel sorun var ama uygulama çalışmaya devam ediyorPayment gateway response slow: 3500ms (threshold: 2000ms)
ERRORBir şeyler ciddi şekilde yanlış gitti. Müdahale gerekebilirFailed 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=30

application.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=DEBUG sadece SQL sorgularını gösterir. Parametre değerlerini de görmek istersen: logging.level.org.hibernate.orm.jdbc.bind=TRACE ekle.

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.xml yerine `logback-spring.xml` kullan. -spring soneki 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:

ElementAçıklamaÖrnek Çıktı
%d{pattern}Tarih/saat2025-01-15 14:30:22.456
%-5levelLog seviyesi (5 karakter, sola hizalı)INFO , ERROR
%threadThread adıhttp-nio-8080-exec-1
%logger{36}Logger adı (max 36 karakter)c.m.service.OrderService
%msgLog mesajıOrder #123 created
%nYeni satır
%highlight()Seviyeye göre renklendirmeERROR=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.gz

  • Dosya 50MB'ı aşarsa gün içinde de bölünür: myapp.2025-01-15.1.log.gz

  • 30 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ğini false bırak. true yapı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=true yoğ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 ThreadLocal tabanlı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=WARN

logback-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), @Slf4j ile devam et. Kullanmıyorsan private static final Logger log = LoggerFactory.getLogger(ClassName.class) standart kalıbını kullan. Değişken adını log yap — logger, LOG, LOGGER karmaş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ıf

logback-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 @Data veya @ToString kullanı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=true

Kullanı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=327ms

Correlation 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.Logger her zaman doğru tercih

  • Parameterized logging yap (log.info("User {} logged in", username)), string concatenation yapma (log.info("User " + username + " logged in")) — performans farkı yüksek trafikte dramatik

  • MDC 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 = ...) kullan

  • Profile-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