# Logowanie w Spring Boot 2026: logi strukturalne na produkcji z Logback i JSON > Kompletny przewodnik po logowaniu strukturalnym w Spring Boot. Konfiguracja Logback JSON, MDC do tracingu, najlepsze praktyki produkcyjne i integracja z ELK Stack. - Published: 2026-03-27 - Updated: 2026-05-04 - Author: SharpSkill - Tags: spring boot logging, logback json, structured logs, elk stack, observability - Reading time: 14 min --- Tradycyjne logi tekstowe szybko stają się niemożliwe do zarządzania na produkcji. Setki instancji generujących tysiące linii na sekundę sprawiają, że wyszukiwanie konkretnego błędu zamienia się w koszmar. Logi strukturalne w formacie JSON zmieniają tę sytuację, ponieważ każde zdarzenie staje się przeszukiwalne i automatycznie analizowalne. > **Najważniejsze** > > Spring Boot 3.4+ natywnie wspiera logowanie strukturalne w JSON bez zewnętrznych zależności. Dla starszych wersji Logback Logstash Encoder pozostaje rozwiązaniem referencyjnym. ## Dlaczego warto wprowadzić logi strukturalne ### Ograniczenia klasycznych logów tekstowych Typowy log tekstowy wygląda tak: ``` 2026-03-27 10:15:32.456 INFO [order-service,abc123] c.e.s.OrderService - Order created for user john@example.com, amount: 150.00€, items: 3 ``` Ten format wywołuje na produkcji liczne problemy. Wyłuskanie konkretnych informacji wymaga skomplikowanych i kruchych wyrażeń regularnych. Korelacja między usługami wymusza ścisłe konwencje, które każdy zespół interpretuje po swojemu. Narzędzia analityczne, takie jak Elasticsearch, mają trudność z efektywnym indeksowaniem tych nieustrukturyzowanych ciągów. ### Zalety formatu JSON To samo zdarzenie w JSON jest natychmiast użyteczne: ```json { "@timestamp": "2026-03-27T10:15:32.456Z", "level": "INFO", "logger": "com.example.service.OrderService", "message": "Order created", "service": "order-service", "traceId": "abc123", "userId": "john@example.com", "orderId": "ORD-789456", "amount": 150.00, "currency": "EUR", "itemCount": 3 } ``` Każde pole staje się filtrowalne i agregowalne. Zapytanie Elasticsearch znajduje natychmiast wszystkie zamówienia powyżej 100 € z ostatnich piętnastu minut. Dashboardy Kibana wizualizują trendy bez ręcznego parsowania. ## Natywna konfiguracja Spring Boot 3.4+ ### Włączenie logów JSON strukturalnych Spring Boot 3.4 wprowadza natywne wsparcie dla logowania strukturalnego dzięki właściwości `logging.structured`. To podejście nie wymaga żadnej dodatkowej zależności. ```yaml # application.yml # Native structured logging configuration for Spring Boot 3.4+ logging: structured: # Output format: ecs (Elastic), logstash, gelf format: console: ecs file: ecs file: name: /var/log/app/application.log level: root: INFO com.example: DEBUG ``` Format ECS (Elastic Common Schema) zapewnia bezpośrednią zgodność z Elasticsearch i Kibana bez dodatkowej konfiguracji. ### Personalizacja pól JSON Aby dodać pola biznesowe do każdego logu, Spring Boot pozwala skonfigurować dodatkowe atrybuty. ```yaml # application.yml # Custom fields in structured logs logging: structured: format: console: ecs ecs: # Service information added to every log service: name: ${spring.application.name} version: ${app.version:1.0.0} environment: ${spring.profiles.active:default} node-name: ${HOSTNAME:unknown} ``` ```java // LoggingConfig.java // Programmatic configuration for additional fields package com.example.logging.config; import org.springframework.boot.logging.structured.StructuredLogFormatterCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class LoggingConfig { @Bean StructuredLogFormatterCustomizer ecsCustomizer() { return formatter -> formatter // Adds static fields to all logs .addStaticField("team", "backend") .addStaticField("region", System.getenv("AWS_REGION")) // Customizes exception formatting .setIncludeStacktrace(true) .setStacktraceMaxLength(5000); } } ``` Te pola pojawiają się w każdej linii logu i ułatwiają filtrowanie po zespole lub regionie w dashboardach. ## Klasyczna konfiguracja Logback z enkoderem JSON ### Zależność Logstash Encoder Dla wersji Spring Boot starszych niż 3.4 lub wymagających zaawansowanej personalizacji Logstash Logback Encoder pozostaje rozwiązaniem referencyjnym. ```xml net.logstash.logback logstash-logback-encoder 7.4 ``` ### Pełna konfiguracja Logback Plik `logback-spring.xml` daje pełną kontrolę nad formatem wyjściowym. ```xml {"service":"${appName}","version":"${appVersion}"} traceId spanId userId requestId yyyy-MM-dd'T'HH:mm:ss.SSSZ 30 4096 36 true /var/log/${appName}/application.json /var/log/${appName}/application.%d{yyyy-MM-dd}.%i.json.gz 30 100MB 3GB {"service":"${appName}","version":"${appVersion}"} %d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n ``` Ta konfiguracja włącza logi JSON tylko na produkcji, zachowując czytelne logi w środowisku deweloperskim. > **Profile Spring** > > Użycie `` umożliwia automatyczne przełączanie między formatem tekstowym a JSON w zależności od środowiska, bez modyfikacji konfiguracji. ## MDC dla tracingu rozproszonego ### Propagacja kontekstu trace MDC (Mapped Diagnostic Context) wzbogaca każdy log o informacje kontekstowe, takie jak identyfikatory żądania lub trace. ```java // TracingFilter.java // Filter for automatic trace context injection package com.example.logging.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TracingFilter extends OncePerRequestFilter { // Standard MDC keys for tracing private static final String TRACE_ID_KEY = "traceId"; private static final String SPAN_ID_KEY = "spanId"; private static final String REQUEST_ID_KEY = "requestId"; private static final String USER_ID_KEY = "userId"; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // Retrieve or generate trace identifiers String traceId = extractOrGenerate(request, "X-Trace-Id", TRACE_ID_KEY); String spanId = generateSpanId(); String requestId = extractOrGenerate(request, "X-Request-Id", REQUEST_ID_KEY); String userId = request.getHeader("X-User-Id"); // Inject into MDC to appear in all logs MDC.put(TRACE_ID_KEY, traceId); MDC.put(SPAN_ID_KEY, spanId); MDC.put(REQUEST_ID_KEY, requestId); if (userId != null) { MDC.put(USER_ID_KEY, userId); } // Propagate to responses for inter-service chaining response.setHeader("X-Trace-Id", traceId); response.setHeader("X-Request-Id", requestId); filterChain.doFilter(request, response); } finally { // Clean MDC after each request MDC.clear(); } } private String extractOrGenerate(HttpServletRequest request, String header, String key) { String value = request.getHeader(header); return value != null ? value : UUID.randomUUID().toString().replace("-", "").substring(0, 16); } private String generateSpanId() { return UUID.randomUUID().toString().replace("-", "").substring(0, 8); } } ``` Każdy log wyemitowany podczas obsługi żądania będzie automatycznie zawierał te identyfikatory. ### Wykorzystanie MDC w kodzie biznesowym ```java // OrderService.java // Business service with enriched contextual logging package com.example.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.stereotype.Service; @Service public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); public Order createOrder(CreateOrderRequest request) { // Add business information to MDC context MDC.put("orderId", request.getOrderId()); MDC.put("customerId", request.getCustomerId()); try { log.info("Creating order with {} items", request.getItems().size()); // Business logic... Order order = processOrder(request); log.info("Order created successfully, total: {} {}", order.getTotal(), order.getCurrency()); return order; } catch (Exception e) { // Exception appears with full MDC context log.error("Failed to create order", e); throw e; } finally { // Clean business keys added MDC.remove("orderId"); MDC.remove("customerId"); } } } ``` Powstały log JSON zawiera wszystkie informacje potrzebne do debugowania: ```json { "@timestamp": "2026-03-27T10:15:32.456Z", "level": "INFO", "logger": "com.example.service.OrderService", "message": "Order created successfully, total: 150.00 EUR", "traceId": "a1b2c3d4e5f67890", "spanId": "12345678", "requestId": "req-abc-123", "userId": "user-456", "orderId": "ORD-789", "customerId": "CUST-321" } ``` ## Logowanie asynchroniczne dla wydajności ### Konfiguracja puli wątków Na produkcji synchroniczne zapisy logów wpływają na latencję żądań. Asynchroniczny appender oddziela logowanie od głównego wątku. ```xml 1024 true 20 false ``` ### Metryki systemu logowania Monitorowanie samego systemu logowania zapobiega cichej utracie logów. ```java // LoggingMetrics.java // Exposing Logback metrics via Micrometer package com.example.logging.metrics; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.classic.AsyncAppender; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import java.util.Iterator; @Component public class LoggingMetrics { private final MeterRegistry registry; public LoggingMetrics(MeterRegistry registry) { this.registry = registry; } @PostConstruct void registerMetrics() { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME); // Iterate through appenders to find AsyncAppenders Iterator> it = rootLogger.iteratorForAppenders(); while (it.hasNext()) { Appender appender = it.next(); if (appender instanceof AsyncAppender asyncAppender) { registerAsyncMetrics(asyncAppender); } } } private void registerAsyncMetrics(AsyncAppender appender) { String appenderName = appender.getName(); // Current queue size Gauge.builder("logback.async.queue.size", appender, AsyncAppender::getQueueSize) .tag("appender", appenderName) .description("Current async appender queue size") .register(registry); // Remaining capacity Gauge.builder("logback.async.queue.remaining", appender, AsyncAppender::getRemainingCapacity) .tag("appender", appenderName) .description("Remaining capacity in async queue") .register(registry); // Number of dropped logs Gauge.builder("logback.async.discarded", appender, AsyncAppender::getNumberOfElementsInQueue) .tag("appender", appenderName) .description("Number of discarded log events") .register(registry); } } ``` Alert Prometheus na `logback.async.queue.remaining < 100` ostrzega przed ryzykiem utraty logów. ## Integracja z ELK Stack ### Konfiguracja Filebeat Filebeat zbiera pliki JSON i przesyła je do Elasticsearch bez transformacji. ```yaml # filebeat.yml # Filebeat configuration for Spring Boot JSON logs filebeat.inputs: - type: log enabled: true paths: - /var/log/*/application.json # Automatic JSON parsing json: keys_under_root: true overwrite_keys: true add_error_key: true message_key: message processors: # Add Kubernetes metadata if available - add_kubernetes_metadata: host: ${NODE_NAME} matchers: - logs_path: logs_path: "/var/log/containers/" # Parse timestamp - timestamp: field: "@timestamp" layouts: - '2006-01-02T15:04:05.000Z' - '2006-01-02T15:04:05.000-07:00' test: - '2026-03-27T10:15:32.456Z' output.elasticsearch: hosts: ["elasticsearch:9200"] index: "logs-%{[service]}-%{+yyyy.MM.dd}" pipeline: "spring-boot-logs" setup.template: name: "logs" pattern: "logs-*" ``` ### Pipeline Elasticsearch do wzbogacania ```json // PUT _ingest/pipeline/spring-boot-logs { "description": "Spring Boot logs enrichment", "processors": [ { "geoip": { "field": "client.ip", "target_field": "client.geo", "ignore_missing": true } }, { "user_agent": { "field": "user_agent.original", "target_field": "user_agent", "ignore_missing": true } }, { "set": { "field": "event.ingested", "value": "{{_ingest.timestamp}}" } }, { "script": { "description": "Classify log level severity", "source": """ def level = ctx.level; if (level == 'ERROR') ctx.severity = 4; else if (level == 'WARN') ctx.severity = 3; else if (level == 'INFO') ctx.severity = 2; else ctx.severity = 1; """ } } ] } ``` ## Najlepsze praktyki produkcyjne ### Informacje, które należy systematycznie umieszczać Każdy log powinien zawierać minimum informacji potrzebnych do debugowania i korelacji. ```java // StructuredLogger.java // Helper for consistent structured logs package com.example.logging; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import java.util.Map; import java.util.function.Supplier; public final class StructuredLogger { private final Logger delegate; private StructuredLogger(Class clazz) { this.delegate = LoggerFactory.getLogger(clazz); } public static StructuredLogger getLogger(Class clazz) { return new StructuredLogger(clazz); } // Log with temporary business context public void info(String message, Map context) { try { context.forEach(MDC::put); delegate.info(message); } finally { context.keySet().forEach(MDC::remove); } } // Log with supplier for lazy evaluation public void debug(Supplier messageSupplier, Map context) { if (delegate.isDebugEnabled()) { try { context.forEach(MDC::put); delegate.debug(messageSupplier.get()); } finally { context.keySet().forEach(MDC::remove); } } } // Error log with full context public void error(String message, Throwable t, Map context) { try { context.forEach(MDC::put); delegate.error(message, t); } finally { context.keySet().forEach(MDC::remove); } } } ``` ```java // Usage in business code private static final StructuredLogger log = StructuredLogger.getLogger(PaymentService.class); public void processPayment(Payment payment) { log.info("Processing payment", Map.of( "paymentId", payment.getId(), "amount", String.valueOf(payment.getAmount()), "currency", payment.getCurrency(), "method", payment.getMethod().name() )); } ``` ### Informacje wrażliwe, które należy wykluczyć Logi nie mogą nigdy zawierać danych osobowych lub wrażliwych. ```java // SensitiveDataFilter.java // Sensitive data masking filter package com.example.logging.filter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; import java.util.regex.Pattern; public class SensitiveDataFilter extends Filter { // Sensitive data patterns to mask private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); private static final Pattern CREDIT_CARD_PATTERN = Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b"); private static final Pattern PASSWORD_PATTERN = Pattern.compile("(?i)(password|pwd|secret|token)[\"']?\\s*[:=]\\s*[\"']?[^\\s,}\"']+"); private static final Pattern PHONE_PATTERN = Pattern.compile("\\+?\\d{1,3}[- ]?\\d{6,14}"); @Override public FilterReply decide(ILoggingEvent event) { // Accept all logs but modify the message // Note: for real masking, use a custom converter return FilterReply.NEUTRAL; } // Utility method to mask data public static String maskSensitiveData(String input) { if (input == null) return null; String result = input; result = EMAIL_PATTERN.matcher(result).replaceAll("[EMAIL_MASKED]"); result = CREDIT_CARD_PATTERN.matcher(result).replaceAll("[CARD_MASKED]"); result = PASSWORD_PATTERN.matcher(result).replaceAll("$1=[REDACTED]"); result = PHONE_PATTERN.matcher(result).replaceAll("[PHONE_MASKED]"); return result; } } ``` > **RODO i zgodność** > > Logi zawierające dane osobowe podlegają RODO. Adresy IP, e-maile i identyfikatory użytkownika wymagają polityki retencji i ewentualnie zgody. ### Odpowiednie poziomy logów ```java // LogLevelGuidelines.java // Appropriate log level guidelines package com.example.logging; public class LogLevelGuidelines { // ERROR: Failure requiring intervention // - Unrecoverable exceptions // - Critical transaction failures // - External service unavailability log.error("Payment gateway unreachable after 3 retries", exception); // WARN: Abnormal but handled situation // - Retry in progress // - Performance degradation // - Resources near limits log.warn("Database connection pool at 85% capacity"); // INFO: Significant business events // - Transaction start/end // - Important state changes // - Key user actions log.info("Order {} shipped to customer {}", orderId, customerId); // DEBUG: Diagnostic information // - Execution details // - Important variable values // - Branching decisions log.debug("Cache miss for key {}, fetching from database", cacheKey); // TRACE: Very fine details // - Method entry/exit // - Complete object contents // - Loops and iterations log.trace("Processing item {} of {}", index, total); } ``` ## Testy i walidacja logów ### Testy jednostkowe struktury JSON ```java // StructuredLoggingTest.java // Structured log validation tests package com.example.logging; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import static org.assertj.core.api.Assertions.assertThat; class StructuredLoggingTest { private ListAppender listAppender; private Logger logger; private ObjectMapper objectMapper; @BeforeEach void setUp() { logger = (Logger) LoggerFactory.getLogger(StructuredLoggingTest.class); listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); objectMapper = new ObjectMapper(); } @Test void shouldIncludeMdcFieldsInLog() { // Given MDC.put("traceId", "test-trace-123"); MDC.put("userId", "user-456"); // When logger.info("Test message with MDC context"); // Then ILoggingEvent event = listAppender.list.get(0); assertThat(event.getMDCPropertyMap()) .containsEntry("traceId", "test-trace-123") .containsEntry("userId", "user-456"); MDC.clear(); } @Test void shouldLogExceptionWithStackTrace() { // Given Exception testException = new RuntimeException("Test error"); // When logger.error("Operation failed", testException); // Then ILoggingEvent event = listAppender.list.get(0); assertThat(event.getThrowableProxy()).isNotNull(); assertThat(event.getThrowableProxy().getMessage()).isEqualTo("Test error"); } } ``` ## Podsumowanie Logi strukturalne JSON przekształcają obserwowalność aplikacji Spring Boot: ✅ **Przeszukiwalne**: każde pole staje się filtrowalne w Elasticsearch lub CloudWatch ✅ **Korelowalne**: MDC propaguje identyfikatory trace pomiędzy usługami ✅ **Wydajne**: asynchroniczny appender oddziela logowanie od przetwarzania ✅ **Bezpieczne**: maskowanie danych wrażliwych zapewnia zgodność z RODO ✅ **Zintegrowane**: natywna kompatybilność z ELK Stack, Datadog, Splunk ✅ **Alertowalne**: ustrukturyzowane pola pozwalają na precyzyjne reguły alertów ✅ **Łatwe w utrzymaniu**: format JSON eliminuje kruche regex parsujące To podejście stanowi fundament nowoczesnej obserwowalności obok metryk (Micrometer) i tracingu rozproszonego (OpenTelemetry). --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/spring-boot/spring-boot-structured-logging-logback-json