# Spring Boot logging en 2026 : logs structurés pour la production avec Logback et JSON > Guide complet des logs structurés Spring Boot. Configuration Logback JSON, MDC pour le tracing, best practices production et intégration ELK Stack. - Published: 2026-03-27 - Updated: 2026-03-31 - Author: SharpSkill - Tags: spring boot logging, logback json, structured logs, elk stack, observabilité - Reading time: 14 min --- Les logs textuels traditionnels deviennent rapidement ingérables en production. Avec des centaines d'instances générant des milliers de lignes par seconde, rechercher une erreur spécifique relève du cauchemar. Les logs structurés en JSON transforment cette situation en rendant chaque événement queryable et analysable automatiquement. > **Point clé** > > Spring Boot 3.4+ supporte nativement les logs structurés JSON sans dépendances externes. Pour les versions antérieures, Logback Logstash Encoder reste la référence. ## Pourquoi adopter les logs structurés ### Limites des logs textuels classiques Un log textuel typique ressemble à ceci : ``` 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 ``` Ce format pose plusieurs problèmes en production. L'extraction d'informations spécifiques nécessite des regex complexes et fragiles. La corrélation entre services requiert des conventions strictes que chaque équipe interprète différemment. Les outils d'analyse comme Elasticsearch peinent à indexer efficacement ces chaînes non structurées. ### Avantages du format JSON Le même événement en JSON devient immédiatement exploitable : ```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 } ``` Chaque champ devient filtrable et agrégeable. Une requête Elasticsearch peut instantanément trouver toutes les commandes supérieures à 100€ du dernier quart d'heure. Les dashboards Kibana visualisent les tendances sans parsing manuel. ## Configuration native Spring Boot 3.4+ ### Activation des logs JSON structurés Spring Boot 3.4 introduit le support natif des logs structurés via la propriété `logging.structured`. Cette approche ne nécessite aucune dépendance supplémentaire. ```yaml # application.yml # Configuration logs structurés natifs Spring Boot 3.4+ logging: structured: # Format de sortie : ecs (Elastic), logstash, gelf format: console: ecs file: ecs file: name: /var/log/app/application.log level: root: INFO com.example: DEBUG ``` Le format ECS (Elastic Common Schema) garantit la compatibilité directe avec Elasticsearch et Kibana sans configuration supplémentaire. ### Personnalisation des champs JSON Pour ajouter des champs métier à chaque log, Spring Boot permet de configurer des attributs additionnels. ```yaml # application.yml # Champs personnalisés dans les logs structurés logging: structured: format: console: ecs ecs: # Informations service ajoutées à chaque log service: name: ${spring.application.name} version: ${app.version:1.0.0} environment: ${spring.profiles.active:default} node-name: ${HOSTNAME:unknown} ``` ```java // LoggingConfig.java // Configuration programmatique des champs additionnels 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 // Ajoute des champs statiques à tous les logs .addStaticField("team", "backend") .addStaticField("region", System.getenv("AWS_REGION")) // Personnalise le formatage des exceptions .setIncludeStacktrace(true) .setStacktraceMaxLength(5000); } } ``` Ces champs apparaissent dans chaque ligne de log, facilitant le filtrage par équipe ou région dans les dashboards. ## Configuration Logback classique avec JSON Encoder ### Dépendance Logstash Encoder Pour les versions Spring Boot antérieures à 3.4 ou les besoins de personnalisation avancée, Logstash Logback Encoder reste la solution de référence. ```xml net.logstash.logback logstash-logback-encoder 7.4 ``` ### Configuration Logback complète Le fichier `logback-spring.xml` offre un contrôle total sur le format de sortie. ```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 ``` Cette configuration active les logs JSON uniquement en production tout en conservant des logs lisibles en développement. > **Profils Spring** > > L'utilisation de `` permet de basculer automatiquement entre formats textuels et JSON selon l'environnement, sans modifier la configuration. ## MDC pour le tracing distribué ### Propagation du contexte de trace Le MDC (Mapped Diagnostic Context) permet d'enrichir chaque log avec des informations de contexte comme l'identifiant de requête ou de trace. ```java // TracingFilter.java // Filtre pour injection automatique du contexte de trace 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 { // Clés MDC standards pour le 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 { // Récupère ou génère les identifiants de trace 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"); // Injecte dans le MDC pour apparaître dans tous les 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); } // Propage aux réponses pour le chaînage inter-services response.setHeader("X-Trace-Id", traceId); response.setHeader("X-Request-Id", requestId); filterChain.doFilter(request, response); } finally { // Nettoie le MDC après chaque requête 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); } } ``` Chaque log émis pendant le traitement de la requête contiendra automatiquement ces identifiants. ### Utilisation du MDC dans le code métier ```java // OrderService.java // Service métier avec logging contextuel enrichi 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) { // Ajoute des informations métier au contexte MDC MDC.put("orderId", request.getOrderId()); MDC.put("customerId", request.getCustomerId()); try { log.info("Creating order with {} items", request.getItems().size()); // Logique métier... Order order = processOrder(request); log.info("Order created successfully, total: {} {}", order.getTotal(), order.getCurrency()); return order; } catch (Exception e) { // L'exception apparaît avec tout le contexte MDC log.error("Failed to create order", e); throw e; } finally { // Nettoie les clés métier ajoutées MDC.remove("orderId"); MDC.remove("customerId"); } } } ``` Le log JSON résultant contient toutes les informations nécessaires pour le debugging : ```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" } ``` ## Logging asynchrone pour la performance ### Configuration du pool de threads En production, les écritures de logs synchrones impactent la latence des requêtes. L'appender asynchrone découple le logging du thread principal. ```xml 1024 true 20 false ``` ### Métriques du système de logging Le monitoring du système de logging lui-même évite les pertes silencieuses de logs. ```java // LoggingMetrics.java // Exposition des métriques Logback 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); // Parcourt les appenders pour trouver les AsyncAppender 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(); // Taille actuelle de la queue Gauge.builder("logback.async.queue.size", appender, AsyncAppender::getQueueSize) .tag("appender", appenderName) .description("Current async appender queue size") .register(registry); // Capacité restante Gauge.builder("logback.async.queue.remaining", appender, AsyncAppender::getRemainingCapacity) .tag("appender", appenderName) .description("Remaining capacity in async queue") .register(registry); // Nombre de logs droppés Gauge.builder("logback.async.discarded", appender, AsyncAppender::getNumberOfElementsInQueue) .tag("appender", appenderName) .description("Number of discarded log events") .register(registry); } } ``` Une alerte Prometheus sur `logback.async.queue.remaining < 100` prévient des risques de perte de logs. ## Intégration ELK Stack ### Configuration Filebeat Filebeat collecte les fichiers JSON et les envoie à Elasticsearch sans transformation. ```yaml # filebeat.yml # Configuration Filebeat pour logs JSON Spring Boot filebeat.inputs: - type: log enabled: true paths: - /var/log/*/application.json # Parse automatique du JSON json: keys_under_root: true overwrite_keys: true add_error_key: true message_key: message processors: # Ajoute des métadonnées Kubernetes si disponible - add_kubernetes_metadata: host: ${NODE_NAME} matchers: - logs_path: logs_path: "/var/log/containers/" # Parse le 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 pour enrichissement ```json // PUT _ingest/pipeline/spring-boot-logs { "description": "Enrichissement logs Spring Boot", "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; """ } } ] } ``` ## Bonnes pratiques pour la production ### Informations à inclure systématiquement Chaque log doit contenir les informations minimales pour le debugging et la corrélation. ```java // StructuredLogger.java // Helper pour logs structurés cohérents 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 avec contexte métier temporaire public void info(String message, Map context) { try { context.forEach(MDC::put); delegate.info(message); } finally { context.keySet().forEach(MDC::remove); } } // Log avec supplier pour évaluation paresseuse 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); } } } // Log d'erreur avec contexte complet 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 // Utilisation dans le code métier 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() )); } ``` ### Informations sensibles à exclure Les logs ne doivent jamais contenir de données personnelles ou sensibles. ```java // SensitiveDataFilter.java // Filtre de masquage des données sensibles 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 { // Patterns de données sensibles à masquer 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) { // Accepte tous les logs mais modifie le message // Note: pour un vrai masquage, utiliser un converter personnalisé return FilterReply.NEUTRAL; } // Méthode utilitaire pour masquer les données 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; } } ``` > **RGPD et conformité** > > Les logs contenant des données personnelles sont soumis au RGPD. Les adresses IP, emails, et identifiants utilisateurs nécessitent une politique de rétention et potentiellement un consentement. ### Niveaux de log appropriés ```java // LogLevelGuidelines.java // Guide des niveaux de log appropriés package com.example.logging; public class LogLevelGuidelines { // ERROR : Échec nécessitant une intervention // - Exceptions non récupérables // - Échecs de transactions critiques // - Indisponibilité de services externes log.error("Payment gateway unreachable after 3 retries", exception); // WARN : Situation anormale mais gérée // - Retry en cours // - Dégradation de performance // - Ressources proches des limites log.warn("Database connection pool at 85% capacity"); // INFO : Événements métier significatifs // - Début/fin de transactions // - Changements d'état importants // - Actions utilisateur clés log.info("Order {} shipped to customer {}", orderId, customerId); // DEBUG : Informations de diagnostic // - Détails d'exécution // - Valeurs de variables importantes // - Décisions de branchement log.debug("Cache miss for key {}, fetching from database", cacheKey); // TRACE : Détails très fins // - Entrée/sortie de méthodes // - Contenu complet des objets // - Boucles et itérations log.trace("Processing item {} of {}", index, total); } ``` ## Tests et validation des logs ### Test unitaire de la structure JSON ```java // StructuredLoggingTest.java // Tests de validation des logs structurés 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"); } } ``` ## Conclusion Les logs structurés JSON transforment l'observabilité des applications Spring Boot : ✅ **Queryable** : chaque champ devient filtrable dans Elasticsearch ou CloudWatch ✅ **Corrélable** : le MDC propage les identifiants de trace entre services ✅ **Performant** : l'appender asynchrone découple le logging du traitement ✅ **Sécurisé** : le masquage des données sensibles assure la conformité RGPD ✅ **Intégré** : compatibilité native avec ELK Stack, Datadog, Splunk ✅ **Alertable** : les champs structurés permettent des règles d'alerte précises ✅ **Maintenable** : le format JSON élimine les regex fragiles de parsing Cette approche constitue le fondement de l'observabilité moderne, aux côtés des métriques (Micrometer) et du tracing distribué (OpenTelemetry). --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/spring-boot/spring-boot-structured-logging-logback-json