Socle V004 – Log4j2 et LogForwarder

Socle V004 - Log4j2 et LogForwarder

22 – Log4j2 et LogForwarder (Nouveauté V4)

Version : 4.0.0 Date : 2025-12-09

1. Introduction

Le Socle V4 remplace Logback par Log4j2 pour le logging, avec un LogForwarder intégré pour la centralisation des logs.

Pourquoi Log4j2 ?

Critère Logback (V3) Log4j2 (V4)
Async natif AsyncAppender (wrapper) AsyncLoggers (LMAX Disruptor)
Performance Bon 6-68x plus rapide
Garbage-free Non Oui
Custom Appender Complexe Plugin system simple
JSON natif Via encoder externe JsonTemplateLayout intégré

2. Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Application                             │
│                                                              │
│  Logger.info("message")                                      │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              Log4j2 AsyncLoggers                     │    │
│  │              (LMAX Disruptor)                        │    │
│  └────────────────────┬────────────────────────────────┘    │
│                       │                                      │
│         ┌─────────────┼─────────────┐                       │
│         ▼             ▼             ▼                       │
│  ┌───────────┐ ┌───────────┐ ┌─────────────────────┐       │
│  │  Console  │ │  File     │ │ SocleLogForwarder   │       │
│  │  Appender │ │  Appender │ │ Appender            │       │
│  └───────────┘ └───────────┘ └──────────┬──────────┘       │
│                                         │                   │
└─────────────────────────────────────────┼───────────────────┘
                                          │
                    ┌─────────────────────┴─────────────────────┐
                    │                                           │
                    ▼                                           ▼
            ┌──────────────┐                           ┌──────────────┐
            │ HTTP Transport│                           │ NATS Transport│
            │ → LogHub     │                           │ → JetStream  │
            └──────┬───────┘                           └──────┬───────┘
                   │                                          │
                   │  (si échec)                              │
                   ▼                                          │
            ┌──────────────┐                                  │
            │ H2 Fallback  │◄─────────────────────────────────┘
            │ Storage      │
            └──────────────┘

3. Configuration

3.1 Dépendances Maven

<!-- Exclure Logback de Spring Boot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- Log4j2 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

<!-- LMAX Disruptor (AsyncLoggers) -->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>4.0.0</version>
</dependency>

<!-- JSON Template Layout -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-layout-template-json</artifactId>
    <version>2.22.1</version>
</dependency>

3.2 log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">

    <Properties>
        <Property name="LOG_DIR">${env:LOG_DIR:-./logs}</Property>
        <Property name="APP_NAME">${env:APP_NAME:-socle-v4}</Property>
        <Property name="REGION">${env:REGION:-local}</Property>
    </Properties>

    <Appenders>
        <!-- Console (dev) -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{ISO8601} %highlight{%-5level} [%thread] %logger{36} - %msg%n"/>
        </Console>

        <!-- Fichier rotatif -->
        <RollingFile name="File"
                     fileName="${LOG_DIR}/${APP_NAME}.log"
                     filePattern="${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- LogForwarder (centralisation) -->
        <SocleLogForwarder name="LogForwarder"
                           transportMode="${env:LOG_TRANSPORT_MODE:-http}"
                           logHubUrl="${env:LOG_HUB_URL:-http://localhost:8080/api/ingest-logs}"
                           natsUrl="${env:NATS_URL:-nats://localhost:4222}"
                           batchSize="100"
                           flushIntervalMs="5000"
                           queueCapacity="10000"
                           serviceName="${APP_NAME}"
                           region="${REGION}">
            <ThresholdFilter level="INFO"/>
        </SocleLogForwarder>
    </Appenders>

    <Loggers>
        <!-- Socle -->
        <Logger name="eu.lmvi.socle" level="${env:LOG_LEVEL:-INFO}" additivity="false">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
            <AppenderRef ref="LogForwarder"/>
        </Logger>

        <!-- Frameworks (moins verbeux) -->
        <Logger name="org.springframework" level="WARN"/>
        <Logger name="org.apache.kafka" level="WARN"/>
        <Logger name="io.nats" level="WARN"/>

        <!-- Root -->
        <Root level="INFO">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
            <AppenderRef ref="LogForwarder"/>
        </Root>
    </Loggers>

</Configuration>

3.3 log4j2.component.properties

# Activer AsyncLoggers globalement (LMAX Disruptor)
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

# Ring buffer (puissance de 2)
AsyncLogger.RingBufferSize=262144

# Politique d'attente
AsyncLogger.WaitStrategy=Sleep

# Sécurité (Log4Shell)
log4j2.formatMsgNoLookups=true

3.4 application.yml

socle:
  logging:
    forwarder:
      enabled: ${LOG_FORWARDER_ENABLED:false}
      transport-mode: ${LOG_TRANSPORT_MODE:http}
      log-hub-url: ${LOG_HUB_URL:http://localhost:8080/api/ingest-logs}
      nats-url: ${NATS_URL:nats://localhost:4222}
      nats-subject-prefix: ${LOG_NATS_SUBJECT:logs}
      batch-size: ${LOG_BATCH_SIZE:100}
      flush-interval-ms: ${LOG_FLUSH_INTERVAL_MS:5000}
      queue-capacity: ${LOG_QUEUE_CAPACITY:10000}

logging:
  config: classpath:log4j2.xml

4. Variables d’environnement

Variable Description Défaut
LOG_LEVEL Niveau de log INFO
LOG_DIR Répertoire des logs ./logs
LOG_FORWARDER_ENABLED Activer LogForwarder false
LOG_TRANSPORT_MODE Mode transport (http/nats) http
LOG_HUB_URL URL du LogHub
NATS_URL URL NATS
LOG_BATCH_SIZE Taille des batches 100
LOG_FLUSH_INTERVAL_MS Intervalle flush (ms) 5000

5. Utilisation

5.1 Logging standard

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

public class MonService {
    private static final Logger log = LoggerFactory.getLogger(MonService.class);

    public void process() {
        log.debug("Processing started");
        log.info("Processing item: {}", itemId);
        log.warn("Slow processing detected");
        log.error("Processing failed", exception);
    }
}

5.2 MDC (Mapped Diagnostic Context)

import org.slf4j.MDC;

public class MonService {
    public void process(String correlationId) {
        MDC.put("correlationId", correlationId);
        MDC.put("worker", "order-processor");
        try {
            log.info("Processing order");
            // Les logs incluront correlationId et worker
        } finally {
            MDC.clear();
        }
    }
}

5.3 Structured logging

// Les logs JSON incluent automatiquement :
// - timestamp
// - level
// - logger
// - thread
// - message
// - MDC (correlationId, execId, etc.)
// - exception (si présente)

log.info("Order processed: orderId={}, amount={}", orderId, amount);

6. LogForwarder

6.1 Principe

Le LogForwarder :

  1. Collecte les logs dans une queue interne (non-bloquant)
  2. Envoie les logs en batch vers le LogHub (HTTP ou NATS)
  3. Stocke en H2 si le réseau est indisponible
  4. Rejoue automatiquement à la reconnexion

6.2 Mode HTTP

socle:
  logging:
    forwarder:
      enabled: true
      transport-mode: http
      log-hub-url: https://logs.mycompany.com/api/ingest-logs

Les logs sont envoyés en POST avec JWT :

POST /api/ingest-logs
Authorization: Bearer <jwt>
Content-Type: application/json

[
  {"timestamp": "...", "level": "INFO", "message": "...", ...},
  {"timestamp": "...", "level": "ERROR", "message": "...", ...}
]

6.3 Mode NATS

socle:
  logging:
    forwarder:
      enabled: true
      transport-mode: nats
      nats-url: nats://nats.mycompany.com:4222
      nats-subject-prefix: logs

Les logs sont publiés sur logs.<region>.<service> :

logs.mtq.order-service
logs.gua.sync-agent

6.4 Fallback H2

Si le transport échoue, les logs sont stockés dans socle_log_fallback :

SELECT COUNT(*) FROM socle_log_fallback;  -- Logs en attente

Ils sont automatiquement rejoués quand le transport redevient disponible.

7. Format JSON des logs

{
  "timestamp": "2025-12-09T10:30:00.123Z",
  "level": "INFO",
  "logger": "eu.lmvi.socle.mop.MainOrchestratorProcess",
  "thread": "main",
  "message": "MOP démarré avec succès",
  "service": "socle-v4",
  "region": "MTQ",
  "instanceId": "nuc-mtq-001",
  "execId": "20251209-1030-abc123",
  "correlationId": "MTQ-2025-12-09-000001",
  "mdc": {
    "worker": "http_worker",
    "phase": "startup"
  },
  "exception": null
}

8. Profils de configuration

8.1 Développement

<!-- log4j2-dev.xml -->
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %highlight{%-5level} [%thread] %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="DEBUG">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

8.2 Production

<!-- log4j2-prod.xml -->
<Configuration status="ERROR">
    <Appenders>
        <Console name="ConsoleJson" target="SYSTEM_OUT">
            <JsonTemplateLayout eventTemplateUri="classpath:socle-log-template.json"/>
        </Console>
        <SocleLogForwarder name="LogForwarder" .../>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="ConsoleJson"/>
            <AppenderRef ref="LogForwarder"/>
        </Root>
    </Loggers>
</Configuration>

8.3 Sélection du profil

logging:
  config: classpath:log4j2-${spring.profiles.active}.xml

9. Performances

AsyncLoggers vs Sync

Mode Throughput Latence
Sync ~1M logs/sec Variable
Async (LMAX) ~18M logs/sec Stable

Bonnes pratiques

// BON - Lazy evaluation
log.debug("Processing: {}", () -> expensiveToString());

// MAUVAIS - Toujours évalué
log.debug("Processing: " + expensiveToString());

10. Troubleshooting

Logs non visibles

  1. Vérifier log4j2.xml dans src/main/resources/
  2. Vérifier les niveaux de log
  3. Vérifier logging.config dans application.yml

AsyncLoggers non activés

Vérifier que log4j2.component.properties existe et contient :

Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

LogForwarder queue pleine

WARN - Log queue full, storing to fallback

Solutions :

  • Augmenter queue-capacity
  • Réduire batch-size
  • Vérifier la connectivité réseau

Logs en fallback non rejoués

-- Vérifier les logs en attente
SELECT COUNT(*) FROM socle_log_fallback;

-- Forcer le replay (via API admin)
POST /admin/logforwarder/replay

11. Sécurité

Log4Shell (CVE-2021-44228)

Log4j2 2.22.1 est protégé contre Log4Shell. De plus :

# Désactiver les lookups JNDI
log4j2.formatMsgNoLookups=true

Données sensibles

// NE PAS logger de données sensibles
log.info("User logged in: {}", user.getEmail());  // OK
log.info("Password: {}", password);  // INTERDIT

12. Références

Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *