← Kursa Dön
📄 Text · 25 min

Structured Logging

Giriş

Logging, bir uygulamanın en temel gözlemleme aracıdır. Metrikler size "ne kadar?" sorusunu cevaplayabilirken, loglar "tam olarak ne oldu?" sorusuna yanıt verir. Ancak tek sunucudaki düz metin logları production ortamının karmaşıklığıyla baş edemez — dağıtık sistemlerde yüzlerce container'dan akan milyonlarca log satırı arasında bir sorunu bulmak, saman yığınında iğne aramaktır. Structured logging (yapılandırılmış loglama), log mesajlarını aranabilir, filtrelenebilir ve analiz edilebilir bir formata dönüştürerek bu problemi çözer.

SLF4J API

SLF4J (Simple Logging Facade for Java), Java'daki logging implementasyonları (Logback, Log4j2 vb.) için bir soyutlama katmanıdır. Spring Boot varsayılan olarak SLF4J + Logback kullanır.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class OrderService {

    // Logger, sınıf bazında oluşturulur
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    // Lombok ile daha kısa:
    // @Slf4j — otomatik 'log' field'ı ekler

    public Order createOrder(OrderRequest request) {
        log.info("Creating order for customer: {}", request.getCustomerId());

        try {
            Order order = processOrder(request);
            log.info("Order created successfully. orderId={}, amount={}",
                     order.getId(), order.getTotalAmount());
            return order;
        } catch (Exception e) {
            log.error("Failed to create order for customer: {}",
                      request.getCustomerId(), e);
            throw e;
        }
    }
}

Placeholder kullanımı: {} placeholder'ları ile parametreler lazy evaluation ile değerlendirilir. String concatenation ("msg: " + value) kullanmayın — log seviyesi kapalıysa bile concatenation yapılır ve performans kaybına neden olur.

Log Seviyeleri

SLF4J beş log seviyesi tanımlar (en düşükten en yükseğe):

SeviyeKullanım Alanı
TRACEEn detaylı seviye. Method giriş/çıkışları, değişken değerleri. Production'da genellikle kapalı.
DEBUGGeliştirme sırasında faydalı detaylar. SQL sorguları, cache hit/miss bilgileri.
INFONormal çalışma olayları. Uygulama başlatma, önemli iş akışı adımları.
WARNPotansiyel problem. Deprecated API kullanımı, retry denemeleri, yavaş sorgular.
ERRORHata durumları. Exception'lar, başarısız işlemler, bağlantı kopmaları.
# application.properties — log seviyesi yapılandırması
logging.level.root=INFO
logging.level.com.example.myapp=DEBUG
logging.level.org.springframework.web=WARN
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

Logback Yapılandırması

Spring Boot, Logback'i varsayılan logging framework'ü olarak kullanır. Basit yapılandırma application.properties ile yapılabilir; ancak gelişmiş senaryolar için logback-spring.xml dosyası kullanılır:

<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- Spring Boot varsayılan yapılandırmasını dahil et -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <!-- Konsol çıktısı — geliştirme ortamı için -->
    <springProfile name="!production">
        <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>

        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>

    <!-- JSON çıktısı — production ortamı için -->
    <springProfile name="production">
        <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeMdcKeyName>requestId</includeMdcKeyName>
                <includeMdcKeyName>userId</includeMdcKeyName>
                <customFields>{"service":"order-service","environment":"production"}</customFields>
            </encoder>
        </appender>

        <!-- Dosyaya yazma — rotasyon ile -->
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>/var/log/myapp/application.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>/var/log/myapp/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
            <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
        </appender>

        <root level="INFO">
            <appender-ref ref="JSON_CONSOLE"/>
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>

</configuration>

JSON formatında log çıktısı için logstash-logback-encoder bağımlılığı gerekir:

<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency>

MDC — Mapped Diagnostic Context

MDC, bir request'in tüm log satırlarına otomatik olarak bağlam bilgisi (context) eklemenizi sağlar. Bir HTTP isteğinin request ID'sini, kullanıcı ID'sini veya correlation ID'sini tüm log satırlarında görmek istiyorsanız MDC kullanırsınız.

@Component
public class RequestLoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // Benzersiz request ID oluştur veya header'dan al
        String requestId = Optional
            .ofNullable(httpRequest.getHeader("X-Request-Id"))
            .orElse(UUID.randomUUID().toString().substring(0, 8));

        String userId = httpRequest.getHeader("X-User-Id");

        try {
            // MDC'ye değerleri koy
            MDC.put("requestId", requestId);
            MDC.put("userId", userId != null ? userId : "anonymous");
            MDC.put("clientIp", httpRequest.getRemoteAddr());

            // Response header'a da ekle (debugging için)
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("X-Request-Id", requestId);

            chain.doFilter(request, response);
        } finally {
            // İstek bittiğinde MDC'yi temizle (thread pool reuse nedeniyle zorunlu!)
            MDC.clear();
        }
    }
}

MDC değerleri otomatik olarak her log satırına eklenir (Logback pattern veya JSON encoder'da yapılandırılmış ise):

// Düz metin formatında:
2024-01-15 10:30:45.123 [http-nio-8080-exec-1] INFO  OrderService - Creating order [requestId=a1b2c3d4, userId=user42]

// JSON formatında:
{
  "@timestamp": "2024-01-15T10:30:45.123Z",
  "level": "INFO",
  "logger_name": "com.example.OrderService",
  "message": "Creating order",
  "requestId": "a1b2c3d4",
  "userId": "user42",
  "clientIp": "192.168.1.100",
  "service": "order-service"
}

Log Correlation — Dağıtık İzleme

Microservice mimarisinde bir kullanıcı isteği birden fazla servis üzerinden geçer. Tüm servisler arasında aynı correlation ID (veya trace ID) kullanarak logları ilişkilendirmek kritik önem taşır:

// Service A → Service B çağrısı
@Service
public class OrderService {

    private final RestClient inventoryClient;

    public void createOrder(OrderRequest request) {
        String traceId = MDC.get("traceId");
        log.info("Creating order, checking inventory...");

        // Trace ID'yi downstream servise propagate et
        inventoryClient.post()
            .uri("/api/inventory/reserve")
            .header("X-Trace-Id", traceId)
            .body(request.getItems())
            .retrieve()
            .toBodilessEntity();
    }
}

Spring Cloud Sleuth veya Micrometer Tracing, bu propagation'ı otomatik yapar. Her log satırında [traceId, spanId] bilgisi görünür.

Best Practices

  1. Hassas verileri loglamayın: Şifreler, kredi kartı numaraları, kişisel bilgiler asla loglara yazılmamalı

  2. Yapılandırılmış format kullanın: Production'da JSON formatı tercih edin; ELK/Loki gibi araçlarla aranabilir olur

  3. MDC ile bağlam ekleyin: Her log satırında requestId, userId gibi bilgiler bulunmalı

  4. Doğru log seviyesini kullanın: Her şeyi INFO yapmayın; DEBUG geliştirme, INFO normal akış, WARN potansiyel sorun, ERROR gerçek hata

  5. Exception'ları doğru loglayın: log.error("msg", exception) — stack trace otomatik eklenir

  6. Performansa dikkat edin: Yoğun döngülerde log yazmaktan kaçının, log.isDebugEnabled() ile koruyun