Catégorie : TheSocle

Tous les elements de documentations sur le socle

  • Test Article – WordPress Publisher System

    title: « Test Article – WordPress Publisher System » date: 2026-01-31T14:30:00 status: publish categories: [2, 3] tags: [5, 6, 11]

    Test Article – WordPress Publisher System

    This is a test article to demonstrate the WordPress Publisher system integration with the Socle V004 framework.

    Introduction

    The WordPress Publisher is an automated content publishing system that:

    • Monitors a local Publications folder for Markdown files
    • Automatically parses and converts Markdown to HTML
    • Publishes content to WordPress via REST API
    • Manages featured images and metadata
    • Tracks changes and updates existing content

    Features

    Automated Scanning

    The system continuously scans the Publications directory and detects new or modified Markdown files.

    Format Support

    • Markdown Parsing: Full Markdown syntax support with tables
    • HTML Sanitization: WordPress-safe HTML output
    • Frontmatter: YAML metadata for article configuration

    WordPress Integration

    • Direct REST API connection
    • Automatic slug generation
    • Category and tag assignment
    • Featured image management
    • Author configuration

    Implementation Details

    The system is built on:

    • Spring Boot 3.2.1 with Java 21
    • Socle V004 framework
    • Docker for containerization
    • Redis for caching (KvBus)
    • H2 Database for state management

    Testing

    This article demonstrates:

    1. ✅ Markdown file scanning
    2. ✅ YAML frontmatter parsing
    3. ✅ HTML conversion
    4. ✅ WordPress API publishing

    Conclusion

    The WordPress Publisher provides a seamless integration between local Markdown content and WordPress, enabling efficient content management workflows.

  • Socle V004 – Plugins

    Socle V004 – Plugins

    20 – Plugins

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 supporte une architecture de plugins pour étendre les fonctionnalités de base. Les plugins sont des modules Spring Boot qui s’intègrent automatiquement.

    2. Architecture des plugins

    ┌──────────────────────────────────────────────────────────┐
    │                    Application                            │
    │                                                           │
    │  ┌─────────────────────────────────────────────────────┐ │
    │  │                   Socle V4 Core                      │ │
    │  │  MOP | Workers | KvBus | TechDB | Logging | etc.    │ │
    │  └─────────────────────────────────────────────────────┘ │
    │                          │                                │
    │         ┌────────────────┼────────────────┐              │
    │         ▼                ▼                ▼              │
    │  ┌────────────┐   ┌────────────┐   ┌────────────┐       │
    │  │   Plugin   │   │   Plugin   │   │   Plugin   │       │
    │  │   Kafka    │   │   NATS     │   │   Custom   │       │
    │  └────────────┘   └────────────┘   └────────────┘       │
    │                                                           │
    └──────────────────────────────────────────────────────────┘
    

    3. Créer un plugin

    3.1 Structure Maven

    <?xml version="1.0" encoding="UTF-8"?>
    <project>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.mycompany</groupId>
        <artifactId>socle-plugin-myplugin</artifactId>
        <version>1.0.0</version>
    
        <dependencies>
            <!-- Dépendance Socle -->
            <dependency>
                <groupId>eu.lmvi</groupId>
                <artifactId>socle-v004</artifactId>
                <version>4.0.0</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </project>
    

    3.2 Auto-configuration

    package com.mycompany.plugin;
    
    import org.springframework.boot.autoconfigure.AutoConfiguration;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.ComponentScan;
    
    @AutoConfiguration
    @ConditionalOnProperty(name = "socle.plugins.myplugin.enabled", havingValue = "true")
    @ComponentScan(basePackages = "com.mycompany.plugin")
    public class MyPluginAutoConfiguration {
        // Configuration automatique
    }
    

    3.3 Fichier spring.factories

    # src/main/resources/META-INF/spring.factories
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.mycompany.plugin.MyPluginAutoConfiguration
    

    Ou pour Spring Boot 3.x :

    # src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    com.mycompany.plugin.MyPluginAutoConfiguration
    

    4. Types de plugins

    4.1 Plugin Worker

    package com.mycompany.plugin.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.plugins.myplugin.enabled", havingValue = "true")
    public class MyPluginWorker implements Worker {
    
        @Override
        public String getName() {
            return "my-plugin-worker";
        }
    
        @Override
        public void initialize() {
            // Initialisation
        }
    
        @Override
        public void start() {
            // Démarrage
        }
    
        @Override
        public void doWork() {
            // Traitement
        }
    
        @Override
        public void stop() {
            // Arrêt
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of();
        }
    }
    

    4.2 Plugin KvBus

    package com.mycompany.plugin.kv;
    
    import eu.lmvi.socle.kv.KvBus;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "custom")
    public class CustomKvBus implements KvBus {
    
        @Override
        public void put(String key, String value) {
            // Implémentation custom
        }
    
        @Override
        public Optional<String> get(String key) {
            // Implémentation custom
            return Optional.empty();
        }
    
        // ... autres méthodes
    }
    

    4.3 Plugin Transport (LogForwarder)

    package com.mycompany.plugin.logging;
    
    import eu.lmvi.socle.logging.LogTransport;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.logging.forwarder.transport-mode", havingValue = "custom")
    public class CustomLogTransport implements LogTransport {
    
        @Override
        public void send(List<LogEntry> entries) throws Exception {
            // Envoyer les logs vers votre système
        }
    
        @Override
        public boolean isAvailable() {
            return true;
        }
    
        @Override
        public void close() {
            // Cleanup
        }
    }
    

    5. Plugin Kafka (exemple complet)

    5.1 Structure

    socle-plugin-kafka/
    ├── pom.xml
    ├── src/main/java/eu/lmvi/socle/plugin/kafka/
    │   ├── KafkaPluginAutoConfiguration.java
    │   ├── KafkaPluginConfiguration.java
    │   ├── KafkaConsumerWorker.java
    │   ├── KafkaProducerService.java
    │   └── KafkaHealthIndicator.java
    └── src/main/resources/
        └── META-INF/spring/
            └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
    

    5.2 Configuration

    @ConfigurationProperties(prefix = "socle.plugins.kafka")
    public class KafkaPluginConfiguration {
        private boolean enabled = false;
        private String bootstrapServers = "localhost:9092";
        private String groupId = "socle-group";
        private List<String> topics = new ArrayList<>();
        private Map<String, String> consumerProperties = new HashMap<>();
        private Map<String, String> producerProperties = new HashMap<>();
    
        // Getters/Setters
    }
    

    5.3 Auto-configuration

    @AutoConfiguration
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    @EnableConfigurationProperties(KafkaPluginConfiguration.class)
    @ComponentScan(basePackages = "eu.lmvi.socle.plugin.kafka")
    public class KafkaPluginAutoConfiguration {
    
        @Bean
        public KafkaConsumer<String, String> kafkaConsumer(KafkaPluginConfiguration config) {
            Properties props = new Properties();
            props.put("bootstrap.servers", config.getBootstrapServers());
            props.put("group.id", config.getGroupId());
            props.putAll(config.getConsumerProperties());
            return new KafkaConsumer<>(props);
        }
    
        @Bean
        public KafkaProducer<String, String> kafkaProducer(KafkaPluginConfiguration config) {
            Properties props = new Properties();
            props.put("bootstrap.servers", config.getBootstrapServers());
            props.putAll(config.getProducerProperties());
            return new KafkaProducer<>(props);
        }
    }
    

    5.4 Worker

    @Component
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaConsumerWorker extends AbstractWorker {
    
        private final KafkaConsumer<String, String> consumer;
        private final KafkaPluginConfiguration config;
        private final TechDbManager techDb;
    
        @Override
        public String getName() {
            return "kafka-consumer-plugin";
        }
    
        @Override
        protected void doInitialize() {
            consumer.subscribe(config.getTopics());
        }
    
        @Override
        protected void doProcess() {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : records) {
                processRecord(record);
            }
        }
    
        @Override
        protected void doStop() {
            consumer.close();
        }
    }
    

    5.5 Utilisation

    # application.yml
    socle:
      plugins:
        kafka:
          enabled: true
          bootstrap-servers: kafka:9092
          group-id: my-app
          topics:
            - orders
            - events
    

    6. Plugin NATS (exemple)

    6.1 Configuration

    @ConfigurationProperties(prefix = "socle.plugins.nats")
    public class NatsPluginConfiguration {
        private boolean enabled = false;
        private String url = "nats://localhost:4222";
        private List<String> subjects = new ArrayList<>();
        private String streamName;
        private String consumerName;
    }
    

    6.2 Worker

    @Component
    @ConditionalOnProperty(name = "socle.plugins.nats.enabled", havingValue = "true")
    public class NatsConsumerWorker extends AbstractWorker {
    
        private final NatsPluginConfiguration config;
        private Connection natsConnection;
        private JetStream jetStream;
    
        @Override
        protected void doInitialize() {
            natsConnection = Nats.connect(config.getUrl());
            jetStream = natsConnection.jetStream();
        }
    
        @Override
        protected void doProcess() {
            for (String subject : config.getSubjects()) {
                Message msg = jetStream.pullSubscribe(subject, config.getConsumerName())
                    .fetch(100, Duration.ofSeconds(1))
                    .stream()
                    .findFirst()
                    .orElse(null);
    
                if (msg != null) {
                    processMessage(msg);
                    msg.ack();
                }
            }
        }
    }
    

    7. Extension des APIs Admin

    7.1 Controller additionnel

    @RestController
    @RequestMapping("/admin/plugins/kafka")
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaAdminController {
    
        @Autowired
        private KafkaConsumerWorker worker;
    
        @GetMapping("/status")
        public Map<String, Object> status() {
            return Map.of(
                "connected", worker.isHealthy(),
                "stats", worker.getStats()
            );
        }
    
        @GetMapping("/offsets")
        public Map<String, Long> offsets() {
            return worker.getCurrentOffsets();
        }
    
        @PostMapping("/seek/{topic}/{partition}/{offset}")
        public void seek(
                @PathVariable String topic,
                @PathVariable int partition,
                @PathVariable long offset) {
            worker.seekTo(topic, partition, offset);
        }
    }
    

    8. Métriques du plugin

    @Component
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaPluginMetrics {
    
        private final Counter messagesReceived;
        private final Counter messagesProcessed;
        private final Timer processingTime;
    
        public KafkaPluginMetrics(MeterRegistry registry) {
            this.messagesReceived = Counter.builder("socle_kafka_messages_received_total")
                .description("Total Kafka messages received")
                .register(registry);
    
            this.messagesProcessed = Counter.builder("socle_kafka_messages_processed_total")
                .description("Total Kafka messages processed")
                .register(registry);
    
            this.processingTime = Timer.builder("socle_kafka_processing_duration_seconds")
                .description("Kafka message processing duration")
                .register(registry);
        }
    
        public void recordReceived() {
            messagesReceived.increment();
        }
    
        public void recordProcessed(Duration duration) {
            messagesProcessed.increment();
            processingTime.record(duration);
        }
    }
    

    9. Test du plugin

    @SpringBootTest
    @TestPropertySource(properties = {
        "socle.plugins.kafka.enabled=true",
        "socle.plugins.kafka.bootstrap-servers=localhost:9092"
    })
    class KafkaPluginTest {
    
        @Autowired
        private KafkaConsumerWorker worker;
    
        @Test
        void workerShouldBeRegistered() {
            assertNotNull(worker);
            assertEquals("kafka-consumer-plugin", worker.getName());
        }
    
        @Test
        void workerShouldStart() {
            worker.initialize();
            worker.start();
            assertTrue(worker.isHealthy());
        }
    }
    

    10. Publication du plugin

    10.1 Maven deploy

    <distributionManagement>
        <repository>
            <id>releases</id>
            <url>https://nexus.mycompany.com/repository/maven-releases/</url>
        </repository>
    </distributionManagement>
    
    mvn clean deploy
    

    10.2 Utilisation dans une application

    <dependency>
        <groupId>eu.lmvi</groupId>
        <artifactId>socle-plugin-kafka</artifactId>
        <version>1.0.0</version>
    </dependency>
    

    11. Bonnes pratiques

    DO

    • Utiliser @ConditionalOnProperty pour activer/désactiver
    • Exposer la configuration via @ConfigurationProperties
    • Implémenter des health indicators
    • Exposer des métriques
    • Documenter les options de configuration

    DON’T

    • Ne pas forcer l’activation par défaut
    • Ne pas dupliquer les fonctionnalités du core
    • Ne pas utiliser de dépendances en conflit avec le Socle
    • Ne pas bloquer le démarrage de l’application si le plugin échoue

    12. Références

  • Socle V004 – KV-Bus

    Socle V004 – KV-Bus

    06 – KvBus (Key-Value Bus)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    KvBus est une abstraction de stockage clé-valeur avec deux implémentations :

    • in_memory : HashMap pour le développement local
    • redis : Redis pour la production multi-instances

    Caractéristiques

    • Interface unifiée
    • TTL (Time-To-Live) configurable
    • Opérations atomiques
    • Support JSON pour les objets complexes
    • Patterns pub/sub (Redis uniquement)

    2. Configuration

    2.1 application.yml

    socle:
      kvbus:
        mode: ${KVBUS_MODE:in_memory}
        redis:
          host: ${REDIS_HOST:localhost}
          port: ${REDIS_PORT:6379}
          password: ${REDIS_PASSWORD:}
          database: ${REDIS_DATABASE:0}
          prefix: ${REDIS_PREFIX:socle}
          connect-timeout-ms: ${REDIS_CONNECT_TIMEOUT:5000}
          read-timeout-ms: ${REDIS_READ_TIMEOUT:5000}
          pool:
            max-total: ${REDIS_POOL_MAX:16}
            max-idle: ${REDIS_POOL_MAX_IDLE:8}
            min-idle: ${REDIS_POOL_MIN_IDLE:2}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    KVBUS_MODE Mode (in_memory/redis) in_memory
    REDIS_HOST Hôte Redis localhost
    REDIS_PORT Port Redis 6379
    REDIS_PASSWORD Mot de passe
    REDIS_DATABASE Database number 0
    REDIS_PREFIX Préfixe des clés socle

    3. Interface KvBus

    package eu.lmvi.socle.kv;
    
    public interface KvBus {
    
        // === CRUD basique ===
    
        /**
         * Stocke une valeur
         */
        void put(String key, String value);
    
        /**
         * Stocke une valeur avec TTL
         */
        void put(String key, String value, Duration ttl);
    
        /**
         * Récupère une valeur
         */
        Optional<String> get(String key);
    
        /**
         * Supprime une clé
         */
        void delete(String key);
    
        /**
         * Vérifie l'existence d'une clé
         */
        boolean exists(String key);
    
        // === TTL ===
    
        /**
         * Définit le TTL d'une clé existante
         */
        void setTtl(String key, Duration ttl);
    
        /**
         * Récupère le TTL restant
         */
        Optional<Duration> getTtl(String key);
    
        // === Opérations atomiques ===
    
        /**
         * Incrémente une valeur numérique
         */
        long increment(String key);
    
        /**
         * Incrémente avec delta
         */
        long increment(String key, long delta);
    
        /**
         * Set if not exists
         */
        boolean putIfAbsent(String key, String value);
    
        /**
         * Set if not exists avec TTL
         */
        boolean putIfAbsent(String key, String value, Duration ttl);
    
        // === Opérations en lot ===
    
        /**
         * Récupère plusieurs clés
         */
        Map<String, String> getAll(Collection<String> keys);
    
        /**
         * Stocke plusieurs valeurs
         */
        void putAll(Map<String, String> entries);
    
        /**
         * Supprime plusieurs clés
         */
        void deleteAll(Collection<String> keys);
    
        // === Pattern matching ===
    
        /**
         * Liste les clés correspondant à un pattern
         */
        Set<String> keys(String pattern);
    
        // === JSON helpers ===
    
        /**
         * Stocke un objet en JSON
         */
        <T> void putJson(String key, T object);
    
        /**
         * Récupère un objet depuis JSON
         */
        <T> Optional<T> getJson(String key, Class<T> type);
    
        // === Lifecycle ===
    
        /**
         * Vérifie la santé de la connexion
         */
        boolean isHealthy();
    
        /**
         * Ferme les connexions
         */
        void close();
    }
    

    4. Implémentation InMemoryKvBus

    package eu.lmvi.socle.kv;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "in_memory", matchIfMissing = true)
    public class InMemoryKvBus implements KvBus {
    
        private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
        private final ScheduledExecutorService cleaner;
    
        public InMemoryKvBus() {
            // Nettoyage des entrées expirées toutes les minutes
            cleaner = Executors.newSingleThreadScheduledExecutor();
            cleaner.scheduleAtFixedRate(this::cleanExpired, 1, 1, TimeUnit.MINUTES);
        }
    
        @Override
        public void put(String key, String value) {
            store.put(key, new Entry(value, null));
        }
    
        @Override
        public void put(String key, String value, Duration ttl) {
            Instant expiry = Instant.now().plus(ttl);
            store.put(key, new Entry(value, expiry));
        }
    
        @Override
        public Optional<String> get(String key) {
            Entry entry = store.get(key);
            if (entry == null) return Optional.empty();
            if (entry.isExpired()) {
                store.remove(key);
                return Optional.empty();
            }
            return Optional.of(entry.value);
        }
    
        @Override
        public void delete(String key) {
            store.remove(key);
        }
    
        @Override
        public boolean exists(String key) {
            return get(key).isPresent();
        }
    
        @Override
        public long increment(String key) {
            return increment(key, 1);
        }
    
        @Override
        public long increment(String key, long delta) {
            Entry entry = store.compute(key, (k, v) -> {
                long current = (v == null) ? 0 : Long.parseLong(v.value);
                return new Entry(String.valueOf(current + delta), v != null ? v.expiry : null);
            });
            return Long.parseLong(entry.value);
        }
    
        @Override
        public boolean putIfAbsent(String key, String value) {
            return store.putIfAbsent(key, new Entry(value, null)) == null;
        }
    
        @Override
        public Set<String> keys(String pattern) {
            String regex = pattern.replace("*", ".*");
            return store.keySet().stream()
                .filter(k -> k.matches(regex))
                .collect(Collectors.toSet());
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public void close() {
            cleaner.shutdown();
            store.clear();
        }
    
        private void cleanExpired() {
            store.entrySet().removeIf(e -> e.getValue().isExpired());
        }
    
        private record Entry(String value, Instant expiry) {
            boolean isExpired() {
                return expiry != null && Instant.now().isAfter(expiry);
            }
        }
    }
    

    5. Implémentation RedisKvBus

    package eu.lmvi.socle.kv;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "redis")
    public class RedisKvBus implements KvBus {
    
        private static final Logger log = LoggerFactory.getLogger(RedisKvBus.class);
    
        private final JedisPool jedisPool;
        private final String prefix;
        private final ObjectMapper objectMapper;
    
        public RedisKvBus(SocleConfiguration config) {
            JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxTotal(config.getKvbus().getRedis().getPool().getMaxTotal());
            poolConfig.setMaxIdle(config.getKvbus().getRedis().getPool().getMaxIdle());
            poolConfig.setMinIdle(config.getKvbus().getRedis().getPool().getMinIdle());
    
            this.jedisPool = new JedisPool(
                poolConfig,
                config.getKvbus().getRedis().getHost(),
                config.getKvbus().getRedis().getPort(),
                config.getKvbus().getRedis().getConnectTimeoutMs(),
                config.getKvbus().getRedis().getPassword(),
                config.getKvbus().getRedis().getDatabase()
            );
    
            this.prefix = config.getKvbus().getRedis().getPrefix() + ":";
            this.objectMapper = new ObjectMapper();
    
            log.info("RedisKvBus initialized: {}:{}",
                config.getKvbus().getRedis().getHost(),
                config.getKvbus().getRedis().getPort());
        }
    
        private String prefixedKey(String key) {
            return prefix + key;
        }
    
        @Override
        public void put(String key, String value) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.set(prefixedKey(key), value);
            }
        }
    
        @Override
        public void put(String key, String value, Duration ttl) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.setex(prefixedKey(key), ttl.toSeconds(), value);
            }
        }
    
        @Override
        public Optional<String> get(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return Optional.ofNullable(jedis.get(prefixedKey(key)));
            }
        }
    
        @Override
        public void delete(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.del(prefixedKey(key));
            }
        }
    
        @Override
        public boolean exists(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.exists(prefixedKey(key));
            }
        }
    
        @Override
        public long increment(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.incr(prefixedKey(key));
            }
        }
    
        @Override
        public long increment(String key, long delta) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.incrBy(prefixedKey(key), delta);
            }
        }
    
        @Override
        public boolean putIfAbsent(String key, String value) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.setnx(prefixedKey(key), value) == 1;
            }
        }
    
        @Override
        public boolean putIfAbsent(String key, String value, Duration ttl) {
            try (Jedis jedis = jedisPool.getResource()) {
                String result = jedis.set(prefixedKey(key), value,
                    SetParams.setParams().nx().ex(ttl.toSeconds()));
                return "OK".equals(result);
            }
        }
    
        @Override
        public Set<String> keys(String pattern) {
            try (Jedis jedis = jedisPool.getResource()) {
                Set<String> rawKeys = jedis.keys(prefixedKey(pattern));
                return rawKeys.stream()
                    .map(k -> k.substring(prefix.length()))
                    .collect(Collectors.toSet());
            }
        }
    
        @Override
        public <T> void putJson(String key, T object) {
            try {
                String json = objectMapper.writeValueAsString(object);
                put(key, json);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize object", e);
            }
        }
    
        @Override
        public <T> Optional<T> getJson(String key, Class<T> type) {
            return get(key).map(json -> {
                try {
                    return objectMapper.readValue(json, type);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException("Failed to deserialize object", e);
                }
            });
        }
    
        @Override
        public boolean isHealthy() {
            try (Jedis jedis = jedisPool.getResource()) {
                return "PONG".equals(jedis.ping());
            } catch (Exception e) {
                return false;
            }
        }
    
        @Override
        public void close() {
            jedisPool.close();
        }
    }
    

    6. Utilisation

    6.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private KvBus kvBus;
    
        public void process() {
            // Utiliser kvBus...
        }
    }
    

    6.2 CRUD basique

    // Stocker
    kvBus.put("user:123:name", "John");
    kvBus.put("session:abc", "data", Duration.ofHours(1));
    
    // Récupérer
    Optional<String> name = kvBus.get("user:123:name");
    name.ifPresent(n -> log.info("Name: {}", n));
    
    // Vérifier
    if (kvBus.exists("user:123:name")) {
        // ...
    }
    
    // Supprimer
    kvBus.delete("user:123:name");
    

    6.3 JSON

    // Stocker un objet
    Order order = new Order("123", "PENDING", List.of("item1", "item2"));
    kvBus.putJson("order:123", order);
    
    // Récupérer un objet
    Optional<Order> retrieved = kvBus.getJson("order:123", Order.class);
    

    6.4 Compteurs atomiques

    // Incrémenter
    long newValue = kvBus.increment("stats:requests:total");
    long newValue2 = kvBus.increment("stats:bytes:total", 1024);
    
    // Compteur avec reset quotidien
    String dailyKey = "stats:requests:" + LocalDate.now();
    kvBus.increment(dailyKey);
    kvBus.setTtl(dailyKey, Duration.ofDays(1));
    

    6.5 Lock distribué (Redis)

    public boolean tryLock(String resource, Duration timeout) {
        String lockKey = "lock:" + resource;
        return kvBus.putIfAbsent(lockKey, "locked", timeout);
    }
    
    public void unlock(String resource) {
        kvBus.delete("lock:" + resource);
    }
    
    // Utilisation
    if (tryLock("order-processing", Duration.ofMinutes(5))) {
        try {
            processOrders();
        } finally {
            unlock("order-processing");
        }
    }
    

    6.6 Cache avec TTL

    public Order getOrder(String orderId) {
        String cacheKey = "cache:order:" + orderId;
    
        // Check cache
        Optional<Order> cached = kvBus.getJson(cacheKey, Order.class);
        if (cached.isPresent()) {
            return cached.get();
        }
    
        // Load from DB
        Order order = orderRepository.findById(orderId);
    
        // Cache for 5 minutes
        kvBus.putJson(cacheKey, order);
        kvBus.setTtl(cacheKey, Duration.ofMinutes(5));
    
        return order;
    }
    

    7. Patterns avancés

    7.1 Rate limiting

    public boolean isRateLimited(String userId, int maxRequests, Duration window) {
        String key = "ratelimit:" + userId + ":" + Instant.now().truncatedTo(ChronoUnit.MINUTES);
    
        long count = kvBus.increment(key);
        if (count == 1) {
            kvBus.setTtl(key, window);
        }
    
        return count > maxRequests;
    }
    

    7.2 Session management

    public void createSession(String sessionId, User user) {
        kvBus.putJson("session:" + sessionId, user);
        kvBus.setTtl("session:" + sessionId, Duration.ofHours(24));
    }
    
    public Optional<User> getSession(String sessionId) {
        return kvBus.getJson("session:" + sessionId, User.class);
    }
    
    public void refreshSession(String sessionId) {
        kvBus.setTtl("session:" + sessionId, Duration.ofHours(24));
    }
    
    public void destroySession(String sessionId) {
        kvBus.delete("session:" + sessionId);
    }
    

    7.3 Feature flags

    public boolean isFeatureEnabled(String feature) {
        return kvBus.get("feature:" + feature)
            .map(Boolean::parseBoolean)
            .orElse(false);
    }
    
    public void setFeatureFlag(String feature, boolean enabled) {
        kvBus.put("feature:" + feature, String.valueOf(enabled));
    }
    

    8. KvBus vs TechDB (V4)

    Aspect KvBus TechDB (H2)
    Cas d’usage Cache, sessions, locks Offsets, état persistant
    Survie restart Non (in_memory) / Oui (Redis) Oui (fichier)
    Multi-instances Non (in_memory) / Oui (Redis) Non (par instance)
    Performance Ultra rapide Rapide
    Requêtes Clé simple SQL, JSON

    Règle de choix

    • KvBus : Données temporaires, cache, sessions, compteurs temps réel
    • TechDB : Offsets, checkpoints, état qui doit survivre au restart

    9. Monitoring

    9.1 Métriques

    socle_kvbus_operations_total{operation="get"}
    socle_kvbus_operations_total{operation="put"}
    socle_kvbus_operations_total{operation="delete"}
    socle_kvbus_latency_seconds{operation="get"}
    socle_kvbus_keys_count
    

    9.2 Health Check

    @Component
    public class KvBusHealthIndicator implements HealthIndicator {
    
        @Autowired
        private KvBus kvBus;
    
        @Override
        public Health health() {
            if (kvBus.isHealthy()) {
                return Health.up().build();
            }
            return Health.down().withDetail("error", "KvBus not responding").build();
        }
    }
    

    10. Bonnes pratiques

    DO

    • Utiliser des préfixes de clés cohérents (user:, session:, cache:)
    • Toujours définir un TTL pour les caches
    • Utiliser putIfAbsent pour les locks
    • Préférer Redis en production multi-instances

    DON’T

    • Ne pas stocker de données volumineuses (> 1MB)
    • Ne pas utiliser keys("*") en production (scan)
    • Ne pas oublier de fermer les connexions
    • Ne pas utiliser in_memory pour les données critiques en prod

    11. Références

  • Socle V004 – Plugins

    Socle V004 – Plugins

    20 – Plugins

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 supporte une architecture de plugins pour étendre les fonctionnalités de base. Les plugins sont des modules Spring Boot qui s’intègrent automatiquement.

    2. Architecture des plugins

    ┌──────────────────────────────────────────────────────────┐
    │                    Application                            │
    │                                                           │
    │  ┌─────────────────────────────────────────────────────┐ │
    │  │                   Socle V4 Core                      │ │
    │  │  MOP | Workers | KvBus | TechDB | Logging | etc.    │ │
    │  └─────────────────────────────────────────────────────┘ │
    │                          │                                │
    │         ┌────────────────┼────────────────┐              │
    │         ▼                ▼                ▼              │
    │  ┌────────────┐   ┌────────────┐   ┌────────────┐       │
    │  │   Plugin   │   │   Plugin   │   │   Plugin   │       │
    │  │   Kafka    │   │   NATS     │   │   Custom   │       │
    │  └────────────┘   └────────────┘   └────────────┘       │
    │                                                           │
    └──────────────────────────────────────────────────────────┘
    

    3. Créer un plugin

    3.1 Structure Maven

    <?xml version="1.0" encoding="UTF-8"?>
    <project>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.mycompany</groupId>
        <artifactId>socle-plugin-myplugin</artifactId>
        <version>1.0.0</version>
    
        <dependencies>
            <!-- Dépendance Socle -->
            <dependency>
                <groupId>eu.lmvi</groupId>
                <artifactId>socle-v004</artifactId>
                <version>4.0.0</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </project>
    

    3.2 Auto-configuration

    package com.mycompany.plugin;
    
    import org.springframework.boot.autoconfigure.AutoConfiguration;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.ComponentScan;
    
    @AutoConfiguration
    @ConditionalOnProperty(name = "socle.plugins.myplugin.enabled", havingValue = "true")
    @ComponentScan(basePackages = "com.mycompany.plugin")
    public class MyPluginAutoConfiguration {
        // Configuration automatique
    }
    

    3.3 Fichier spring.factories

    # src/main/resources/META-INF/spring.factories
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.mycompany.plugin.MyPluginAutoConfiguration
    

    Ou pour Spring Boot 3.x :

    # src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    com.mycompany.plugin.MyPluginAutoConfiguration
    

    4. Types de plugins

    4.1 Plugin Worker

    package com.mycompany.plugin.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.plugins.myplugin.enabled", havingValue = "true")
    public class MyPluginWorker implements Worker {
    
        @Override
        public String getName() {
            return "my-plugin-worker";
        }
    
        @Override
        public void initialize() {
            // Initialisation
        }
    
        @Override
        public void start() {
            // Démarrage
        }
    
        @Override
        public void doWork() {
            // Traitement
        }
    
        @Override
        public void stop() {
            // Arrêt
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of();
        }
    }
    

    4.2 Plugin KvBus

    package com.mycompany.plugin.kv;
    
    import eu.lmvi.socle.kv.KvBus;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "custom")
    public class CustomKvBus implements KvBus {
    
        @Override
        public void put(String key, String value) {
            // Implémentation custom
        }
    
        @Override
        public Optional<String> get(String key) {
            // Implémentation custom
            return Optional.empty();
        }
    
        // ... autres méthodes
    }
    

    4.3 Plugin Transport (LogForwarder)

    package com.mycompany.plugin.logging;
    
    import eu.lmvi.socle.logging.LogTransport;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.logging.forwarder.transport-mode", havingValue = "custom")
    public class CustomLogTransport implements LogTransport {
    
        @Override
        public void send(List<LogEntry> entries) throws Exception {
            // Envoyer les logs vers votre système
        }
    
        @Override
        public boolean isAvailable() {
            return true;
        }
    
        @Override
        public void close() {
            // Cleanup
        }
    }
    

    5. Plugin Kafka (exemple complet)

    5.1 Structure

    socle-plugin-kafka/
    ├── pom.xml
    ├── src/main/java/eu/lmvi/socle/plugin/kafka/
    │   ├── KafkaPluginAutoConfiguration.java
    │   ├── KafkaPluginConfiguration.java
    │   ├── KafkaConsumerWorker.java
    │   ├── KafkaProducerService.java
    │   └── KafkaHealthIndicator.java
    └── src/main/resources/
        └── META-INF/spring/
            └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
    

    5.2 Configuration

    @ConfigurationProperties(prefix = "socle.plugins.kafka")
    public class KafkaPluginConfiguration {
        private boolean enabled = false;
        private String bootstrapServers = "localhost:9092";
        private String groupId = "socle-group";
        private List<String> topics = new ArrayList<>();
        private Map<String, String> consumerProperties = new HashMap<>();
        private Map<String, String> producerProperties = new HashMap<>();
    
        // Getters/Setters
    }
    

    5.3 Auto-configuration

    @AutoConfiguration
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    @EnableConfigurationProperties(KafkaPluginConfiguration.class)
    @ComponentScan(basePackages = "eu.lmvi.socle.plugin.kafka")
    public class KafkaPluginAutoConfiguration {
    
        @Bean
        public KafkaConsumer<String, String> kafkaConsumer(KafkaPluginConfiguration config) {
            Properties props = new Properties();
            props.put("bootstrap.servers", config.getBootstrapServers());
            props.put("group.id", config.getGroupId());
            props.putAll(config.getConsumerProperties());
            return new KafkaConsumer<>(props);
        }
    
        @Bean
        public KafkaProducer<String, String> kafkaProducer(KafkaPluginConfiguration config) {
            Properties props = new Properties();
            props.put("bootstrap.servers", config.getBootstrapServers());
            props.putAll(config.getProducerProperties());
            return new KafkaProducer<>(props);
        }
    }
    

    5.4 Worker

    @Component
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaConsumerWorker extends AbstractWorker {
    
        private final KafkaConsumer<String, String> consumer;
        private final KafkaPluginConfiguration config;
        private final TechDbManager techDb;
    
        @Override
        public String getName() {
            return "kafka-consumer-plugin";
        }
    
        @Override
        protected void doInitialize() {
            consumer.subscribe(config.getTopics());
        }
    
        @Override
        protected void doProcess() {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : records) {
                processRecord(record);
            }
        }
    
        @Override
        protected void doStop() {
            consumer.close();
        }
    }
    

    5.5 Utilisation

    # application.yml
    socle:
      plugins:
        kafka:
          enabled: true
          bootstrap-servers: kafka:9092
          group-id: my-app
          topics:
            - orders
            - events
    

    6. Plugin NATS (exemple)

    6.1 Configuration

    @ConfigurationProperties(prefix = "socle.plugins.nats")
    public class NatsPluginConfiguration {
        private boolean enabled = false;
        private String url = "nats://localhost:4222";
        private List<String> subjects = new ArrayList<>();
        private String streamName;
        private String consumerName;
    }
    

    6.2 Worker

    @Component
    @ConditionalOnProperty(name = "socle.plugins.nats.enabled", havingValue = "true")
    public class NatsConsumerWorker extends AbstractWorker {
    
        private final NatsPluginConfiguration config;
        private Connection natsConnection;
        private JetStream jetStream;
    
        @Override
        protected void doInitialize() {
            natsConnection = Nats.connect(config.getUrl());
            jetStream = natsConnection.jetStream();
        }
    
        @Override
        protected void doProcess() {
            for (String subject : config.getSubjects()) {
                Message msg = jetStream.pullSubscribe(subject, config.getConsumerName())
                    .fetch(100, Duration.ofSeconds(1))
                    .stream()
                    .findFirst()
                    .orElse(null);
    
                if (msg != null) {
                    processMessage(msg);
                    msg.ack();
                }
            }
        }
    }
    

    7. Extension des APIs Admin

    7.1 Controller additionnel

    @RestController
    @RequestMapping("/admin/plugins/kafka")
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaAdminController {
    
        @Autowired
        private KafkaConsumerWorker worker;
    
        @GetMapping("/status")
        public Map<String, Object> status() {
            return Map.of(
                "connected", worker.isHealthy(),
                "stats", worker.getStats()
            );
        }
    
        @GetMapping("/offsets")
        public Map<String, Long> offsets() {
            return worker.getCurrentOffsets();
        }
    
        @PostMapping("/seek/{topic}/{partition}/{offset}")
        public void seek(
                @PathVariable String topic,
                @PathVariable int partition,
                @PathVariable long offset) {
            worker.seekTo(topic, partition, offset);
        }
    }
    

    8. Métriques du plugin

    @Component
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaPluginMetrics {
    
        private final Counter messagesReceived;
        private final Counter messagesProcessed;
        private final Timer processingTime;
    
        public KafkaPluginMetrics(MeterRegistry registry) {
            this.messagesReceived = Counter.builder("socle_kafka_messages_received_total")
                .description("Total Kafka messages received")
                .register(registry);
    
            this.messagesProcessed = Counter.builder("socle_kafka_messages_processed_total")
                .description("Total Kafka messages processed")
                .register(registry);
    
            this.processingTime = Timer.builder("socle_kafka_processing_duration_seconds")
                .description("Kafka message processing duration")
                .register(registry);
        }
    
        public void recordReceived() {
            messagesReceived.increment();
        }
    
        public void recordProcessed(Duration duration) {
            messagesProcessed.increment();
            processingTime.record(duration);
        }
    }
    

    9. Test du plugin

    @SpringBootTest
    @TestPropertySource(properties = {
        "socle.plugins.kafka.enabled=true",
        "socle.plugins.kafka.bootstrap-servers=localhost:9092"
    })
    class KafkaPluginTest {
    
        @Autowired
        private KafkaConsumerWorker worker;
    
        @Test
        void workerShouldBeRegistered() {
            assertNotNull(worker);
            assertEquals("kafka-consumer-plugin", worker.getName());
        }
    
        @Test
        void workerShouldStart() {
            worker.initialize();
            worker.start();
            assertTrue(worker.isHealthy());
        }
    }
    

    10. Publication du plugin

    10.1 Maven deploy

    <distributionManagement>
        <repository>
            <id>releases</id>
            <url>https://nexus.mycompany.com/repository/maven-releases/</url>
        </repository>
    </distributionManagement>
    
    mvn clean deploy
    

    10.2 Utilisation dans une application

    <dependency>
        <groupId>eu.lmvi</groupId>
        <artifactId>socle-plugin-kafka</artifactId>
        <version>1.0.0</version>
    </dependency>
    

    11. Bonnes pratiques

    DO

    • Utiliser @ConditionalOnProperty pour activer/désactiver
    • Exposer la configuration via @ConfigurationProperties
    • Implémenter des health indicators
    • Exposer des métriques
    • Documenter les options de configuration

    DON’T

    • Ne pas forcer l’activation par défaut
    • Ne pas dupliquer les fonctionnalités du core
    • Ne pas utiliser de dépendances en conflit avec le Socle
    • Ne pas bloquer le démarrage de l’application si le plugin échoue

    12. Références

  • Socle V004 – KV-Bus

    Socle V004 – KV-Bus

    06 – KvBus (Key-Value Bus)

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    KvBus est une abstraction de stockage clé-valeur avec deux implémentations :

    • in_memory : HashMap pour le développement local
    • redis : Redis pour la production multi-instances

    Caractéristiques

    • Interface unifiée
    • TTL (Time-To-Live) configurable
    • Opérations atomiques
    • Support JSON pour les objets complexes
    • Patterns pub/sub (Redis uniquement)

    2. Configuration

    2.1 application.yml

    socle:
      kvbus:
        mode: ${KVBUS_MODE:in_memory}
        redis:
          host: ${REDIS_HOST:localhost}
          port: ${REDIS_PORT:6379}
          password: ${REDIS_PASSWORD:}
          database: ${REDIS_DATABASE:0}
          prefix: ${REDIS_PREFIX:socle}
          connect-timeout-ms: ${REDIS_CONNECT_TIMEOUT:5000}
          read-timeout-ms: ${REDIS_READ_TIMEOUT:5000}
          pool:
            max-total: ${REDIS_POOL_MAX:16}
            max-idle: ${REDIS_POOL_MAX_IDLE:8}
            min-idle: ${REDIS_POOL_MIN_IDLE:2}
    

    2.2 Variables d’environnement

    Variable Description Défaut
    KVBUS_MODE Mode (in_memory/redis) in_memory
    REDIS_HOST Hôte Redis localhost
    REDIS_PORT Port Redis 6379
    REDIS_PASSWORD Mot de passe
    REDIS_DATABASE Database number 0
    REDIS_PREFIX Préfixe des clés socle

    3. Interface KvBus

    package eu.lmvi.socle.kv;
    
    public interface KvBus {
    
        // === CRUD basique ===
    
        /**
         * Stocke une valeur
         */
        void put(String key, String value);
    
        /**
         * Stocke une valeur avec TTL
         */
        void put(String key, String value, Duration ttl);
    
        /**
         * Récupère une valeur
         */
        Optional<String> get(String key);
    
        /**
         * Supprime une clé
         */
        void delete(String key);
    
        /**
         * Vérifie l'existence d'une clé
         */
        boolean exists(String key);
    
        // === TTL ===
    
        /**
         * Définit le TTL d'une clé existante
         */
        void setTtl(String key, Duration ttl);
    
        /**
         * Récupère le TTL restant
         */
        Optional<Duration> getTtl(String key);
    
        // === Opérations atomiques ===
    
        /**
         * Incrémente une valeur numérique
         */
        long increment(String key);
    
        /**
         * Incrémente avec delta
         */
        long increment(String key, long delta);
    
        /**
         * Set if not exists
         */
        boolean putIfAbsent(String key, String value);
    
        /**
         * Set if not exists avec TTL
         */
        boolean putIfAbsent(String key, String value, Duration ttl);
    
        // === Opérations en lot ===
    
        /**
         * Récupère plusieurs clés
         */
        Map<String, String> getAll(Collection<String> keys);
    
        /**
         * Stocke plusieurs valeurs
         */
        void putAll(Map<String, String> entries);
    
        /**
         * Supprime plusieurs clés
         */
        void deleteAll(Collection<String> keys);
    
        // === Pattern matching ===
    
        /**
         * Liste les clés correspondant à un pattern
         */
        Set<String> keys(String pattern);
    
        // === JSON helpers ===
    
        /**
         * Stocke un objet en JSON
         */
        <T> void putJson(String key, T object);
    
        /**
         * Récupère un objet depuis JSON
         */
        <T> Optional<T> getJson(String key, Class<T> type);
    
        // === Lifecycle ===
    
        /**
         * Vérifie la santé de la connexion
         */
        boolean isHealthy();
    
        /**
         * Ferme les connexions
         */
        void close();
    }
    

    4. Implémentation InMemoryKvBus

    package eu.lmvi.socle.kv;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "in_memory", matchIfMissing = true)
    public class InMemoryKvBus implements KvBus {
    
        private final ConcurrentHashMap<String, Entry> store = new ConcurrentHashMap<>();
        private final ScheduledExecutorService cleaner;
    
        public InMemoryKvBus() {
            // Nettoyage des entrées expirées toutes les minutes
            cleaner = Executors.newSingleThreadScheduledExecutor();
            cleaner.scheduleAtFixedRate(this::cleanExpired, 1, 1, TimeUnit.MINUTES);
        }
    
        @Override
        public void put(String key, String value) {
            store.put(key, new Entry(value, null));
        }
    
        @Override
        public void put(String key, String value, Duration ttl) {
            Instant expiry = Instant.now().plus(ttl);
            store.put(key, new Entry(value, expiry));
        }
    
        @Override
        public Optional<String> get(String key) {
            Entry entry = store.get(key);
            if (entry == null) return Optional.empty();
            if (entry.isExpired()) {
                store.remove(key);
                return Optional.empty();
            }
            return Optional.of(entry.value);
        }
    
        @Override
        public void delete(String key) {
            store.remove(key);
        }
    
        @Override
        public boolean exists(String key) {
            return get(key).isPresent();
        }
    
        @Override
        public long increment(String key) {
            return increment(key, 1);
        }
    
        @Override
        public long increment(String key, long delta) {
            Entry entry = store.compute(key, (k, v) -> {
                long current = (v == null) ? 0 : Long.parseLong(v.value);
                return new Entry(String.valueOf(current + delta), v != null ? v.expiry : null);
            });
            return Long.parseLong(entry.value);
        }
    
        @Override
        public boolean putIfAbsent(String key, String value) {
            return store.putIfAbsent(key, new Entry(value, null)) == null;
        }
    
        @Override
        public Set<String> keys(String pattern) {
            String regex = pattern.replace("*", ".*");
            return store.keySet().stream()
                .filter(k -> k.matches(regex))
                .collect(Collectors.toSet());
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public void close() {
            cleaner.shutdown();
            store.clear();
        }
    
        private void cleanExpired() {
            store.entrySet().removeIf(e -> e.getValue().isExpired());
        }
    
        private record Entry(String value, Instant expiry) {
            boolean isExpired() {
                return expiry != null && Instant.now().isAfter(expiry);
            }
        }
    }
    

    5. Implémentation RedisKvBus

    package eu.lmvi.socle.kv;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "redis")
    public class RedisKvBus implements KvBus {
    
        private static final Logger log = LoggerFactory.getLogger(RedisKvBus.class);
    
        private final JedisPool jedisPool;
        private final String prefix;
        private final ObjectMapper objectMapper;
    
        public RedisKvBus(SocleConfiguration config) {
            JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxTotal(config.getKvbus().getRedis().getPool().getMaxTotal());
            poolConfig.setMaxIdle(config.getKvbus().getRedis().getPool().getMaxIdle());
            poolConfig.setMinIdle(config.getKvbus().getRedis().getPool().getMinIdle());
    
            this.jedisPool = new JedisPool(
                poolConfig,
                config.getKvbus().getRedis().getHost(),
                config.getKvbus().getRedis().getPort(),
                config.getKvbus().getRedis().getConnectTimeoutMs(),
                config.getKvbus().getRedis().getPassword(),
                config.getKvbus().getRedis().getDatabase()
            );
    
            this.prefix = config.getKvbus().getRedis().getPrefix() + ":";
            this.objectMapper = new ObjectMapper();
    
            log.info("RedisKvBus initialized: {}:{}",
                config.getKvbus().getRedis().getHost(),
                config.getKvbus().getRedis().getPort());
        }
    
        private String prefixedKey(String key) {
            return prefix + key;
        }
    
        @Override
        public void put(String key, String value) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.set(prefixedKey(key), value);
            }
        }
    
        @Override
        public void put(String key, String value, Duration ttl) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.setex(prefixedKey(key), ttl.toSeconds(), value);
            }
        }
    
        @Override
        public Optional<String> get(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return Optional.ofNullable(jedis.get(prefixedKey(key)));
            }
        }
    
        @Override
        public void delete(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.del(prefixedKey(key));
            }
        }
    
        @Override
        public boolean exists(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.exists(prefixedKey(key));
            }
        }
    
        @Override
        public long increment(String key) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.incr(prefixedKey(key));
            }
        }
    
        @Override
        public long increment(String key, long delta) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.incrBy(prefixedKey(key), delta);
            }
        }
    
        @Override
        public boolean putIfAbsent(String key, String value) {
            try (Jedis jedis = jedisPool.getResource()) {
                return jedis.setnx(prefixedKey(key), value) == 1;
            }
        }
    
        @Override
        public boolean putIfAbsent(String key, String value, Duration ttl) {
            try (Jedis jedis = jedisPool.getResource()) {
                String result = jedis.set(prefixedKey(key), value,
                    SetParams.setParams().nx().ex(ttl.toSeconds()));
                return "OK".equals(result);
            }
        }
    
        @Override
        public Set<String> keys(String pattern) {
            try (Jedis jedis = jedisPool.getResource()) {
                Set<String> rawKeys = jedis.keys(prefixedKey(pattern));
                return rawKeys.stream()
                    .map(k -> k.substring(prefix.length()))
                    .collect(Collectors.toSet());
            }
        }
    
        @Override
        public <T> void putJson(String key, T object) {
            try {
                String json = objectMapper.writeValueAsString(object);
                put(key, json);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize object", e);
            }
        }
    
        @Override
        public <T> Optional<T> getJson(String key, Class<T> type) {
            return get(key).map(json -> {
                try {
                    return objectMapper.readValue(json, type);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException("Failed to deserialize object", e);
                }
            });
        }
    
        @Override
        public boolean isHealthy() {
            try (Jedis jedis = jedisPool.getResource()) {
                return "PONG".equals(jedis.ping());
            } catch (Exception e) {
                return false;
            }
        }
    
        @Override
        public void close() {
            jedisPool.close();
        }
    }
    

    6. Utilisation

    6.1 Injection

    @Service
    public class MonService {
    
        @Autowired
        private KvBus kvBus;
    
        public void process() {
            // Utiliser kvBus...
        }
    }
    

    6.2 CRUD basique

    // Stocker
    kvBus.put("user:123:name", "John");
    kvBus.put("session:abc", "data", Duration.ofHours(1));
    
    // Récupérer
    Optional<String> name = kvBus.get("user:123:name");
    name.ifPresent(n -> log.info("Name: {}", n));
    
    // Vérifier
    if (kvBus.exists("user:123:name")) {
        // ...
    }
    
    // Supprimer
    kvBus.delete("user:123:name");
    

    6.3 JSON

    // Stocker un objet
    Order order = new Order("123", "PENDING", List.of("item1", "item2"));
    kvBus.putJson("order:123", order);
    
    // Récupérer un objet
    Optional<Order> retrieved = kvBus.getJson("order:123", Order.class);
    

    6.4 Compteurs atomiques

    // Incrémenter
    long newValue = kvBus.increment("stats:requests:total");
    long newValue2 = kvBus.increment("stats:bytes:total", 1024);
    
    // Compteur avec reset quotidien
    String dailyKey = "stats:requests:" + LocalDate.now();
    kvBus.increment(dailyKey);
    kvBus.setTtl(dailyKey, Duration.ofDays(1));
    

    6.5 Lock distribué (Redis)

    public boolean tryLock(String resource, Duration timeout) {
        String lockKey = "lock:" + resource;
        return kvBus.putIfAbsent(lockKey, "locked", timeout);
    }
    
    public void unlock(String resource) {
        kvBus.delete("lock:" + resource);
    }
    
    // Utilisation
    if (tryLock("order-processing", Duration.ofMinutes(5))) {
        try {
            processOrders();
        } finally {
            unlock("order-processing");
        }
    }
    

    6.6 Cache avec TTL

    public Order getOrder(String orderId) {
        String cacheKey = "cache:order:" + orderId;
    
        // Check cache
        Optional<Order> cached = kvBus.getJson(cacheKey, Order.class);
        if (cached.isPresent()) {
            return cached.get();
        }
    
        // Load from DB
        Order order = orderRepository.findById(orderId);
    
        // Cache for 5 minutes
        kvBus.putJson(cacheKey, order);
        kvBus.setTtl(cacheKey, Duration.ofMinutes(5));
    
        return order;
    }
    

    7. Patterns avancés

    7.1 Rate limiting

    public boolean isRateLimited(String userId, int maxRequests, Duration window) {
        String key = "ratelimit:" + userId + ":" + Instant.now().truncatedTo(ChronoUnit.MINUTES);
    
        long count = kvBus.increment(key);
        if (count == 1) {
            kvBus.setTtl(key, window);
        }
    
        return count > maxRequests;
    }
    

    7.2 Session management

    public void createSession(String sessionId, User user) {
        kvBus.putJson("session:" + sessionId, user);
        kvBus.setTtl("session:" + sessionId, Duration.ofHours(24));
    }
    
    public Optional<User> getSession(String sessionId) {
        return kvBus.getJson("session:" + sessionId, User.class);
    }
    
    public void refreshSession(String sessionId) {
        kvBus.setTtl("session:" + sessionId, Duration.ofHours(24));
    }
    
    public void destroySession(String sessionId) {
        kvBus.delete("session:" + sessionId);
    }
    

    7.3 Feature flags

    public boolean isFeatureEnabled(String feature) {
        return kvBus.get("feature:" + feature)
            .map(Boolean::parseBoolean)
            .orElse(false);
    }
    
    public void setFeatureFlag(String feature, boolean enabled) {
        kvBus.put("feature:" + feature, String.valueOf(enabled));
    }
    

    8. KvBus vs TechDB (V4)

    Aspect KvBus TechDB (H2)
    Cas d’usage Cache, sessions, locks Offsets, état persistant
    Survie restart Non (in_memory) / Oui (Redis) Oui (fichier)
    Multi-instances Non (in_memory) / Oui (Redis) Non (par instance)
    Performance Ultra rapide Rapide
    Requêtes Clé simple SQL, JSON

    Règle de choix

    • KvBus : Données temporaires, cache, sessions, compteurs temps réel
    • TechDB : Offsets, checkpoints, état qui doit survivre au restart

    9. Monitoring

    9.1 Métriques

    socle_kvbus_operations_total{operation="get"}
    socle_kvbus_operations_total{operation="put"}
    socle_kvbus_operations_total{operation="delete"}
    socle_kvbus_latency_seconds{operation="get"}
    socle_kvbus_keys_count
    

    9.2 Health Check

    @Component
    public class KvBusHealthIndicator implements HealthIndicator {
    
        @Autowired
        private KvBus kvBus;
    
        @Override
        public Health health() {
            if (kvBus.isHealthy()) {
                return Health.up().build();
            }
            return Health.down().withDetail("error", "KvBus not responding").build();
        }
    }
    

    10. Bonnes pratiques

    DO

    • Utiliser des préfixes de clés cohérents (user:, session:, cache:)
    • Toujours définir un TTL pour les caches
    • Utiliser putIfAbsent pour les locks
    • Préférer Redis en production multi-instances

    DON’T

    • Ne pas stocker de données volumineuses (> 1MB)
    • Ne pas utiliser keys("*") en production (scan)
    • Ne pas oublier de fermer les connexions
    • Ne pas utiliser in_memory pour les données critiques en prod

    11. Références

  • Socle V004 – Plugins

    Socle V004 – Plugins

    20 – Plugins

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Le Socle V4 supporte une architecture de plugins pour étendre les fonctionnalités de base. Les plugins sont des modules Spring Boot qui s’intègrent automatiquement.

    2. Architecture des plugins

    ┌──────────────────────────────────────────────────────────┐
    │                    Application                            │
    │                                                           │
    │  ┌─────────────────────────────────────────────────────┐ │
    │  │                   Socle V4 Core                      │ │
    │  │  MOP | Workers | KvBus | TechDB | Logging | etc.    │ │
    │  └─────────────────────────────────────────────────────┘ │
    │                          │                                │
    │         ┌────────────────┼────────────────┐              │
    │         ▼                ▼                ▼              │
    │  ┌────────────┐   ┌────────────┐   ┌────────────┐       │
    │  │   Plugin   │   │   Plugin   │   │   Plugin   │       │
    │  │   Kafka    │   │   NATS     │   │   Custom   │       │
    │  └────────────┘   └────────────┘   └────────────┘       │
    │                                                           │
    └──────────────────────────────────────────────────────────┘
    

    3. Créer un plugin

    3.1 Structure Maven

    <?xml version="1.0" encoding="UTF-8"?>
    <project>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.mycompany</groupId>
        <artifactId>socle-plugin-myplugin</artifactId>
        <version>1.0.0</version>
    
        <dependencies>
            <!-- Dépendance Socle -->
            <dependency>
                <groupId>eu.lmvi</groupId>
                <artifactId>socle-v004</artifactId>
                <version>4.0.0</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </project>
    

    3.2 Auto-configuration

    package com.mycompany.plugin;
    
    import org.springframework.boot.autoconfigure.AutoConfiguration;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.ComponentScan;
    
    @AutoConfiguration
    @ConditionalOnProperty(name = "socle.plugins.myplugin.enabled", havingValue = "true")
    @ComponentScan(basePackages = "com.mycompany.plugin")
    public class MyPluginAutoConfiguration {
        // Configuration automatique
    }
    

    3.3 Fichier spring.factories

    # src/main/resources/META-INF/spring.factories
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.mycompany.plugin.MyPluginAutoConfiguration
    

    Ou pour Spring Boot 3.x :

    # src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    com.mycompany.plugin.MyPluginAutoConfiguration
    

    4. Types de plugins

    4.1 Plugin Worker

    package com.mycompany.plugin.worker;
    
    import eu.lmvi.socle.worker.Worker;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.plugins.myplugin.enabled", havingValue = "true")
    public class MyPluginWorker implements Worker {
    
        @Override
        public String getName() {
            return "my-plugin-worker";
        }
    
        @Override
        public void initialize() {
            // Initialisation
        }
    
        @Override
        public void start() {
            // Démarrage
        }
    
        @Override
        public void doWork() {
            // Traitement
        }
    
        @Override
        public void stop() {
            // Arrêt
        }
    
        @Override
        public boolean isHealthy() {
            return true;
        }
    
        @Override
        public Map<String, Object> getStats() {
            return Map.of();
        }
    }
    

    4.2 Plugin KvBus

    package com.mycompany.plugin.kv;
    
    import eu.lmvi.socle.kv.KvBus;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.kvbus.mode", havingValue = "custom")
    public class CustomKvBus implements KvBus {
    
        @Override
        public void put(String key, String value) {
            // Implémentation custom
        }
    
        @Override
        public Optional<String> get(String key) {
            // Implémentation custom
            return Optional.empty();
        }
    
        // ... autres méthodes
    }
    

    4.3 Plugin Transport (LogForwarder)

    package com.mycompany.plugin.logging;
    
    import eu.lmvi.socle.logging.LogTransport;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConditionalOnProperty(name = "socle.logging.forwarder.transport-mode", havingValue = "custom")
    public class CustomLogTransport implements LogTransport {
    
        @Override
        public void send(List<LogEntry> entries) throws Exception {
            // Envoyer les logs vers votre système
        }
    
        @Override
        public boolean isAvailable() {
            return true;
        }
    
        @Override
        public void close() {
            // Cleanup
        }
    }
    

    5. Plugin Kafka (exemple complet)

    5.1 Structure

    socle-plugin-kafka/
    ├── pom.xml
    ├── src/main/java/eu/lmvi/socle/plugin/kafka/
    │   ├── KafkaPluginAutoConfiguration.java
    │   ├── KafkaPluginConfiguration.java
    │   ├── KafkaConsumerWorker.java
    │   ├── KafkaProducerService.java
    │   └── KafkaHealthIndicator.java
    └── src/main/resources/
        └── META-INF/spring/
            └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
    

    5.2 Configuration

    @ConfigurationProperties(prefix = "socle.plugins.kafka")
    public class KafkaPluginConfiguration {
        private boolean enabled = false;
        private String bootstrapServers = "localhost:9092";
        private String groupId = "socle-group";
        private List<String> topics = new ArrayList<>();
        private Map<String, String> consumerProperties = new HashMap<>();
        private Map<String, String> producerProperties = new HashMap<>();
    
        // Getters/Setters
    }
    

    5.3 Auto-configuration

    @AutoConfiguration
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    @EnableConfigurationProperties(KafkaPluginConfiguration.class)
    @ComponentScan(basePackages = "eu.lmvi.socle.plugin.kafka")
    public class KafkaPluginAutoConfiguration {
    
        @Bean
        public KafkaConsumer<String, String> kafkaConsumer(KafkaPluginConfiguration config) {
            Properties props = new Properties();
            props.put("bootstrap.servers", config.getBootstrapServers());
            props.put("group.id", config.getGroupId());
            props.putAll(config.getConsumerProperties());
            return new KafkaConsumer<>(props);
        }
    
        @Bean
        public KafkaProducer<String, String> kafkaProducer(KafkaPluginConfiguration config) {
            Properties props = new Properties();
            props.put("bootstrap.servers", config.getBootstrapServers());
            props.putAll(config.getProducerProperties());
            return new KafkaProducer<>(props);
        }
    }
    

    5.4 Worker

    @Component
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaConsumerWorker extends AbstractWorker {
    
        private final KafkaConsumer<String, String> consumer;
        private final KafkaPluginConfiguration config;
        private final TechDbManager techDb;
    
        @Override
        public String getName() {
            return "kafka-consumer-plugin";
        }
    
        @Override
        protected void doInitialize() {
            consumer.subscribe(config.getTopics());
        }
    
        @Override
        protected void doProcess() {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for (ConsumerRecord<String, String> record : records) {
                processRecord(record);
            }
        }
    
        @Override
        protected void doStop() {
            consumer.close();
        }
    }
    

    5.5 Utilisation

    # application.yml
    socle:
      plugins:
        kafka:
          enabled: true
          bootstrap-servers: kafka:9092
          group-id: my-app
          topics:
            - orders
            - events
    

    6. Plugin NATS (exemple)

    6.1 Configuration

    @ConfigurationProperties(prefix = "socle.plugins.nats")
    public class NatsPluginConfiguration {
        private boolean enabled = false;
        private String url = "nats://localhost:4222";
        private List<String> subjects = new ArrayList<>();
        private String streamName;
        private String consumerName;
    }
    

    6.2 Worker

    @Component
    @ConditionalOnProperty(name = "socle.plugins.nats.enabled", havingValue = "true")
    public class NatsConsumerWorker extends AbstractWorker {
    
        private final NatsPluginConfiguration config;
        private Connection natsConnection;
        private JetStream jetStream;
    
        @Override
        protected void doInitialize() {
            natsConnection = Nats.connect(config.getUrl());
            jetStream = natsConnection.jetStream();
        }
    
        @Override
        protected void doProcess() {
            for (String subject : config.getSubjects()) {
                Message msg = jetStream.pullSubscribe(subject, config.getConsumerName())
                    .fetch(100, Duration.ofSeconds(1))
                    .stream()
                    .findFirst()
                    .orElse(null);
    
                if (msg != null) {
                    processMessage(msg);
                    msg.ack();
                }
            }
        }
    }
    

    7. Extension des APIs Admin

    7.1 Controller additionnel

    @RestController
    @RequestMapping("/admin/plugins/kafka")
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaAdminController {
    
        @Autowired
        private KafkaConsumerWorker worker;
    
        @GetMapping("/status")
        public Map<String, Object> status() {
            return Map.of(
                "connected", worker.isHealthy(),
                "stats", worker.getStats()
            );
        }
    
        @GetMapping("/offsets")
        public Map<String, Long> offsets() {
            return worker.getCurrentOffsets();
        }
    
        @PostMapping("/seek/{topic}/{partition}/{offset}")
        public void seek(
                @PathVariable String topic,
                @PathVariable int partition,
                @PathVariable long offset) {
            worker.seekTo(topic, partition, offset);
        }
    }
    

    8. Métriques du plugin

    @Component
    @ConditionalOnProperty(name = "socle.plugins.kafka.enabled", havingValue = "true")
    public class KafkaPluginMetrics {
    
        private final Counter messagesReceived;
        private final Counter messagesProcessed;
        private final Timer processingTime;
    
        public KafkaPluginMetrics(MeterRegistry registry) {
            this.messagesReceived = Counter.builder("socle_kafka_messages_received_total")
                .description("Total Kafka messages received")
                .register(registry);
    
            this.messagesProcessed = Counter.builder("socle_kafka_messages_processed_total")
                .description("Total Kafka messages processed")
                .register(registry);
    
            this.processingTime = Timer.builder("socle_kafka_processing_duration_seconds")
                .description("Kafka message processing duration")
                .register(registry);
        }
    
        public void recordReceived() {
            messagesReceived.increment();
        }
    
        public void recordProcessed(Duration duration) {
            messagesProcessed.increment();
            processingTime.record(duration);
        }
    }
    

    9. Test du plugin

    @SpringBootTest
    @TestPropertySource(properties = {
        "socle.plugins.kafka.enabled=true",
        "socle.plugins.kafka.bootstrap-servers=localhost:9092"
    })
    class KafkaPluginTest {
    
        @Autowired
        private KafkaConsumerWorker worker;
    
        @Test
        void workerShouldBeRegistered() {
            assertNotNull(worker);
            assertEquals("kafka-consumer-plugin", worker.getName());
        }
    
        @Test
        void workerShouldStart() {
            worker.initialize();
            worker.start();
            assertTrue(worker.isHealthy());
        }
    }
    

    10. Publication du plugin

    10.1 Maven deploy

    <distributionManagement>
        <repository>
            <id>releases</id>
            <url>https://nexus.mycompany.com/repository/maven-releases/</url>
        </repository>
    </distributionManagement>
    
    mvn clean deploy
    

    10.2 Utilisation dans une application

    <dependency>
        <groupId>eu.lmvi</groupId>
        <artifactId>socle-plugin-kafka</artifactId>
        <version>1.0.0</version>
    </dependency>
    

    11. Bonnes pratiques

    DO

    • Utiliser @ConditionalOnProperty pour activer/désactiver
    • Exposer la configuration via @ConfigurationProperties
    • Implémenter des health indicators
    • Exposer des métriques
    • Documenter les options de configuration

    DON’T

    • Ne pas forcer l’activation par défaut
    • Ne pas dupliquer les fonctionnalités du core
    • Ne pas utiliser de dépendances en conflit avec le Socle
    • Ne pas bloquer le démarrage de l’application si le plugin échoue

    12. Références

  • Socle V004 – Kubernetes

    Socle V004 – Kubernetes

    16 – Kubernetes

    Version : 4.0.0 Date : 2025-12-09

    1. Introduction

    Guide de déploiement du Socle V4 sur Kubernetes.

    2. Image Docker

    2.1 Dockerfile

    FROM eclipse-temurin:21-jre-alpine
    
    LABEL maintainer="your-team@company.com"
    LABEL version="4.0.0"
    
    WORKDIR /app
    
    # Non-root user
    RUN addgroup -S socle && adduser -S socle -G socle
    USER socle
    
    # Copy application
    COPY --chown=socle:socle target/socle-v004-4.0.0.jar app.jar
    
    # Health check
    HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
      CMD wget -qO- http://localhost:8080/admin/health/live || exit 1
    
    # Default environment
    ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
    
    EXPOSE 8080
    
    ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
    

    2.2 Build et Push

    # Build
    docker build -t gcr.io/my-project/socle-v4:4.0.0 .
    
    # Push
    docker push gcr.io/my-project/socle-v4:4.0.0
    

    3. Manifests Kubernetes

    3.1 Namespace

    apiVersion: v1
    kind: Namespace
    metadata:
      name: socle
      labels:
        name: socle
    

    3.2 ConfigMap

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: socle-config
      namespace: socle
    data:
      APP_NAME: "socle-v4"
      ENV_NAME: "PROD"
      REGION: "europe-west1"
      HTTP_PORT: "8080"
      KVBUS_MODE: "redis"
      REDIS_HOST: "redis-master.redis.svc.cluster.local"
      TECHDB_ENABLED: "true"
      LOG_FORWARDER_ENABLED: "true"
      LOG_TRANSPORT_MODE: "http"
      SCHEDULER_ENABLED: "true"
      ADMIN_ENABLED: "true"
      ADMIN_AUTH_ENABLED: "true"
    

    3.3 Secret

    apiVersion: v1
    kind: Secret
    metadata:
      name: socle-secrets
      namespace: socle
    type: Opaque
    stringData:
      REDIS_PASSWORD: "your-redis-password"
      ADMIN_PASSWORD: "your-admin-password"
      API_KEY: "your-api-key"
      TECHDB_PASSWORD: "your-techdb-password"
    

    3.4 Deployment

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: socle-v4
      namespace: socle
      labels:
        app: socle-v4
        version: "4.0.0"
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: socle-v4
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 0
      template:
        metadata:
          labels:
            app: socle-v4
            version: "4.0.0"
          annotations:
            prometheus.io/scrape: "true"
            prometheus.io/path: "/actuator/prometheus"
            prometheus.io/port: "8080"
        spec:
          serviceAccountName: socle-sa
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            fsGroup: 1000
          containers:
            - name: socle
              image: gcr.io/my-project/socle-v4:4.0.0
              imagePullPolicy: Always
              ports:
                - name: http
                  containerPort: 8080
                  protocol: TCP
              envFrom:
                - configMapRef:
                    name: socle-config
                - secretRef:
                    name: socle-secrets
              env:
                - name: POD_NAME
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.name
                - name: POD_NAMESPACE
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.namespace
                - name: EXEC_ID
                  value: "$(POD_NAME)"
              resources:
                requests:
                  cpu: "250m"
                  memory: "512Mi"
                limits:
                  cpu: "1000m"
                  memory: "1Gi"
              livenessProbe:
                httpGet:
                  path: /admin/health/live
                  port: 8080
                initialDelaySeconds: 30
                periodSeconds: 10
                timeoutSeconds: 5
                failureThreshold: 3
              readinessProbe:
                httpGet:
                  path: /admin/health/ready
                  port: 8080
                initialDelaySeconds: 10
                periodSeconds: 5
                timeoutSeconds: 3
                failureThreshold: 3
              volumeMounts:
                - name: data
                  mountPath: /app/data
                - name: logs
                  mountPath: /app/logs
          volumes:
            - name: data
              emptyDir: {}
            - name: logs
              emptyDir: {}
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
                - weight: 100
                  podAffinityTerm:
                    labelSelector:
                      matchLabels:
                        app: socle-v4
                    topologyKey: kubernetes.io/hostname
    

    3.5 Service

    apiVersion: v1
    kind: Service
    metadata:
      name: socle-v4
      namespace: socle
      labels:
        app: socle-v4
    spec:
      type: ClusterIP
      ports:
        - name: http
          port: 80
          targetPort: 8080
          protocol: TCP
      selector:
        app: socle-v4
    

    3.6 Ingress

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: socle-v4
      namespace: socle
      annotations:
        kubernetes.io/ingress.class: nginx
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
        cert-manager.io/cluster-issuer: letsencrypt-prod
    spec:
      tls:
        - hosts:
            - socle.example.com
          secretName: socle-tls
      rules:
        - host: socle.example.com
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: socle-v4
                    port:
                      number: 80
    

    3.7 HorizontalPodAutoscaler

    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: socle-v4
      namespace: socle
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: socle-v4
      minReplicas: 2
      maxReplicas: 10
      metrics:
        - type: Resource
          resource:
            name: cpu
            target:
              type: Utilization
              averageUtilization: 70
        - type: Resource
          resource:
            name: memory
            target:
              type: Utilization
              averageUtilization: 80
    

    3.8 PodDisruptionBudget

    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: socle-v4
      namespace: socle
    spec:
      minAvailable: 1
      selector:
        matchLabels:
          app: socle-v4
    

    3.9 ServiceAccount

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: socle-sa
      namespace: socle
    

    4. Persistence avec PVC

    4.1 PersistentVolumeClaim

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: socle-data
      namespace: socle
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: standard
      resources:
        requests:
          storage: 10Gi
    

    4.2 Deployment avec PVC

    # Dans le Deployment
    spec:
      template:
        spec:
          containers:
            - name: socle
              volumeMounts:
                - name: data
                  mountPath: /app/data
          volumes:
            - name: data
              persistentVolumeClaim:
                claimName: socle-data
    

    5. Network Policies

    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: socle-network-policy
      namespace: socle
    spec:
      podSelector:
        matchLabels:
          app: socle-v4
      policyTypes:
        - Ingress
        - Egress
      ingress:
        # Allow from ingress controller
        - from:
            - namespaceSelector:
                matchLabels:
                  name: ingress-nginx
          ports:
            - port: 8080
        # Allow from Prometheus
        - from:
            - namespaceSelector:
                matchLabels:
                  name: monitoring
          ports:
            - port: 8080
      egress:
        # Allow to Redis
        - to:
            - namespaceSelector:
                matchLabels:
                  name: redis
          ports:
            - port: 6379
        # Allow to DNS
        - to:
            - namespaceSelector: {}
              podSelector:
                matchLabels:
                  k8s-app: kube-dns
          ports:
            - port: 53
              protocol: UDP
    

    6. Helm Chart

    6.1 Chart.yaml

    apiVersion: v2
    name: socle-v4
    description: Socle V4 Framework
    version: 4.0.0
    appVersion: "4.0.0"
    

    6.2 values.yaml

    replicaCount: 2
    
    image:
      repository: gcr.io/my-project/socle-v4
      tag: "4.0.0"
      pullPolicy: Always
    
    service:
      type: ClusterIP
      port: 80
    
    ingress:
      enabled: true
      className: nginx
      hosts:
        - host: socle.example.com
          paths:
            - path: /
              pathType: Prefix
      tls:
        - secretName: socle-tls
          hosts:
            - socle.example.com
    
    resources:
      requests:
        cpu: 250m
        memory: 512Mi
      limits:
        cpu: 1000m
        memory: 1Gi
    
    autoscaling:
      enabled: true
      minReplicas: 2
      maxReplicas: 10
      targetCPUUtilizationPercentage: 70
    
    config:
      APP_NAME: socle-v4
      ENV_NAME: PROD
      KVBUS_MODE: redis
    
    secrets:
      REDIS_PASSWORD: ""
      ADMIN_PASSWORD: ""
      API_KEY: ""
    

    6.3 Installation

    # Install
    helm install socle-v4 ./socle-v4-chart -n socle --create-namespace -f values-prod.yaml
    
    # Upgrade
    helm upgrade socle-v4 ./socle-v4-chart -n socle -f values-prod.yaml
    
    # Uninstall
    helm uninstall socle-v4 -n socle
    

    7. Observability

    7.1 ServiceMonitor (Prometheus Operator)

    apiVersion: monitoring.coreos.com/v1
    kind: ServiceMonitor
    metadata:
      name: socle-v4
      namespace: socle
      labels:
        release: prometheus
    spec:
      selector:
        matchLabels:
          app: socle-v4
      endpoints:
        - port: http
          path: /actuator/prometheus
          interval: 15s
    

    7.2 PrometheusRule

    apiVersion: monitoring.coreos.com/v1
    kind: PrometheusRule
    metadata:
      name: socle-v4-alerts
      namespace: socle
    spec:
      groups:
        - name: socle-v4
          rules:
            - alert: SocleHighErrorRate
              expr: rate(socle_errors_total[5m]) > 0.1
              for: 5m
              labels:
                severity: warning
              annotations:
                summary: High error rate
    

    8. Déploiement Multi-région

    8.1 Structure

    clusters/
    ├── europe-west1/
    │   ├── kustomization.yaml
    │   └── config-patch.yaml
    ├── us-central1/
    │   ├── kustomization.yaml
    │   └── config-patch.yaml
    └── base/
        ├── kustomization.yaml
        ├── deployment.yaml
        ├── service.yaml
        └── configmap.yaml
    

    8.2 Kustomize overlay

    # clusters/europe-west1/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    bases:
      - ../../base
    patchesStrategicMerge:
      - config-patch.yaml
    configMapGenerator:
      - name: socle-config
        behavior: merge
        literals:
          - REGION=europe-west1
    

    9. Troubleshooting

    Commandes utiles

    # Logs
    kubectl logs -f deployment/socle-v4 -n socle
    
    # Describe pod
    kubectl describe pod -l app=socle-v4 -n socle
    
    # Port forward
    kubectl port-forward svc/socle-v4 8080:80 -n socle
    
    # Exec into pod
    kubectl exec -it deployment/socle-v4 -n socle -- sh
    
    # Check events
    kubectl get events -n socle --sort-by='.lastTimestamp'
    

    10. Références

  • 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

  • Socle V004 – H2 et TechDB

    Socle V004 – H2 et TechDB

    21 – H2 TechDB (Nouveaute V4)

    Version : 4.0.2 Date : 2026-01-15

    1. Introduction

    La H2 TechDB est une base de donnees embarquee introduite dans le Socle V4 pour stocker l’etat technique de maniere persistante.

    Pourquoi H2 ?

    Critere H2 Nitrite (ancien)
    Embarque
    ARM/AMD64 ⚠️ Problemes
    UI debug ✅ H2 Console
    JSON SQL ✅ JSON_VALUE
    Stabilite ⚠️ v4 instable

    Nouveautes V4.0.1

    • Standard de tables x_ : Nouvelle structure avec champs techniques standardises
    • H2 Console sur port 9376 : Interface web dediee pour explorer la base
    • TechDbReaderWorker : Worker passif de lecture des donnees
    • TechDbPurgeWorker : Purge automatique des donnees obsoletes

    Nouveautes V4.0.2

    • API SQL REST : Nouveau endpoint /techdb/query pour requetes SQL via HTTP
    • Authentification Basic Auth : Securisation de l’acces API
    • Rate Limiting : Protection contre les abus (60 req/min/IP par defaut)
    • Mode Readonly : Protection contre les modifications accidentelles

    2. Cas d’usage

    La TechDB stocke :

    • Offsets/sequences : Position dans Kafka, NATS, DB2
    • Etat local des workers : Statut, derniere execution
    • Evenements techniques : Logs importants
    • Fallback logs : Logs non envoyes (LogForwarder)
    • Cle-valeur : Donnees generiques avec TTL

    3. Configuration

    3.1 application.yml

    socle:
      techdb:
        enabled: ${TECHDB_ENABLED:true}
        url: jdbc:h2:file:${TECHDB_PATH:./data/socle-techdb};MODE=PostgreSQL;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE
        username: ${TECHDB_USERNAME:LMVI}
        password: ${TECHDB_PASSWORD:LMVI-SOCLEV004}
    
        # H2 Console Web (port 9376)
        console:
          enabled: ${TECHDB_CONSOLE_ENABLED:true}
          port: ${TECHDB_CONSOLE_PORT:9376}
          bind_address: ${TECHDB_CONSOLE_BIND:}
          allow_remote: ${TECHDB_CONSOLE_ALLOW_REMOTE:false}
    
        # Purge automatique des donnees anciennes
        purge:
          enabled: ${TECHDB_PURGE_ENABLED:true}
          schedule: ${TECHDB_PURGE_SCHEDULE:0 0 3 * * ?}  # 3h du matin
          events_retention_days: ${TECHDB_PURGE_EVENTS_DAYS:7}
          logs_retention_days: ${TECHDB_PURGE_LOGS_DAYS:3}
    

    3.2 Variables d’environnement

    Variable Description Defaut
    TECHDB_ENABLED Activer TechDB true
    TECHDB_PATH Chemin fichier H2 ./data/socle-techdb
    TECHDB_USERNAME Nom d’utilisateur LMVI
    TECHDB_PASSWORD Mot de passe LMVI-SOCLEV004
    TECHDB_CONSOLE_ENABLED Activer console web true
    TECHDB_CONSOLE_PORT Port console 9376
    TECHDB_CONSOLE_ALLOW_REMOTE Acces distant false
    TECHDB_PURGE_ENABLED Activer purge auto true
    TECHDB_PURGE_EVENTS_DAYS Retention events 7
    TECHDB_PURGE_LOGS_DAYS Retention logs 3

    3.3 Personnalisation des identifiants par environnement

    Les identifiants par defaut (LMVI / LMVI-SOCLEV004) conviennent pour le developpement et les tests. Pour la production, il est recommande de personnaliser via variables d’environnement.

    Docker Compose :

    services:
      mon-app:
        environment:
          - TECHDB_USERNAME=MonUserProd
          - TECHDB_PASSWORD=MonMotDePasseSecurise123!
    

    Java direct :

    export TECHDB_USERNAME=MonUserProd
    export TECHDB_PASSWORD=MonMotDePasseSecurise123!
    java -jar mon-application.jar
    

    Kubernetes :

    env:
      - name: TECHDB_USERNAME
        valueFrom:
          secretKeyRef:
            name: techdb-credentials
            key: username
      - name: TECHDB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: techdb-credentials
            key: password
    

    Note : Si vous changez les identifiants sur une base existante, vous devez supprimer le fichier socle-techdb.mv.db et laisser l’application recreer la base.

    4. Schéma de base

    Les tables sont créées automatiquement au démarrage :

    -- Offsets / séquences
    CREATE TABLE IF NOT EXISTS socle_offsets (
        id IDENTITY PRIMARY KEY,
        source_name VARCHAR(200) NOT NULL,
        partition_key VARCHAR(200) NOT NULL,
        last_sequence BIGINT NOT NULL,
        last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        extra JSON,
        UNIQUE(source_name, partition_key)
    );
    
    -- État local des workers
    CREATE TABLE IF NOT EXISTS socle_worker_state (
        id IDENTITY PRIMARY KEY,
        worker_id VARCHAR(200) NOT NULL UNIQUE,
        status VARCHAR(20) NOT NULL,
        last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        metadata JSON
    );
    
    -- Événements techniques
    CREATE TABLE IF NOT EXISTS socle_events (
        id IDENTITY PRIMARY KEY,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        type VARCHAR(100) NOT NULL,
        payload JSON
    );
    
    -- Fallback logs (LogForwarder)
    CREATE TABLE IF NOT EXISTS socle_log_fallback (
        id IDENTITY PRIMARY KEY,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        log_entry JSON NOT NULL,
        retry_count INT DEFAULT 0
    );
    

    5. Interface TechDbManager

    package eu.lmvi.socle.techdb;
    
    @Component
    public class TechDbManager {
    
        // ===== Lifecycle =====
    
        /**
         * Initialise la base H2 et crée les tables
         */
        public void initialize();
    
        /**
         * Ferme proprement la connexion
         */
        public void close();
    
        /**
         * Vérifie la santé de la base
         */
        public boolean isHealthy();
    
        // ===== Offsets =====
    
        /**
         * Sauvegarde un offset
         */
        public void saveOffset(String sourceName, String partitionKey,
                               long sequence, Map<String, Object> extra);
    
        /**
         * Récupère un offset
         */
        public OptionalLong getOffset(String sourceName, String partitionKey);
    
        /**
         * Liste tous les offsets d'une source
         */
        public List<OffsetRecord> getOffsets(String sourceName);
    
        // ===== Worker State =====
    
        /**
         * Sauvegarde l'état d'un worker
         */
        public void saveWorkerState(String workerId, String status,
                                    Map<String, Object> metadata);
    
        /**
         * Récupère l'état d'un worker
         */
        public Optional<WorkerState> getWorkerState(String workerId);
    
        // ===== Events =====
    
        /**
         * Enregistre un événement technique
         */
        public void logEvent(String type, Map<String, Object> payload);
    
        /**
         * Récupère les événements
         */
        public List<TechEvent> getEvents(String type, Instant since, int limit);
    
        // ===== Generic KV =====
    
        public void put(String key, String value);
        public Optional<String> get(String key);
        public void delete(String key);
    }
    

    6. Utilisation

    6.1 Injection

    @Service
    public class MonWorker implements Worker {
    
        @Autowired
        private TechDbManager techDb;
    
        @Override
        public void doWork() {
            // Récupérer le dernier offset
            OptionalLong lastOffset = techDb.getOffset("kafka", "my-topic-0");
            long startFrom = lastOffset.orElse(0L);
    
            // Traiter les messages...
            long newOffset = processMessages(startFrom);
    
            // Sauvegarder le nouvel offset
            techDb.saveOffset("kafka", "my-topic-0", newOffset,
                Map.of("processed", true));
        }
    }
    

    6.2 Gestion des offsets

    // Sauvegarder
    techDb.saveOffset("nats", "events.orders", 123456789L,
        Map.of("consumer", "order-processor"));
    
    // Récupérer
    OptionalLong offset = techDb.getOffset("nats", "events.orders");
    if (offset.isPresent()) {
        log.info("Dernier offset: {}", offset.getAsLong());
    }
    
    // Lister tous les offsets NATS
    List<OffsetRecord> offsets = techDb.getOffsets("nats");
    offsets.forEach(o -> log.info("{}: {}", o.partitionKey(), o.lastSequence()));
    

    6.3 État des workers

    // Sauvegarder l'état
    techDb.saveWorkerState("kafka-consumer-001", "RUNNING",
        Map.of(
            "lastProcessed", Instant.now(),
            "messagesPerMinute", 523
        ));
    
    // Récupérer l'état
    Optional<WorkerState> state = techDb.getWorkerState("kafka-consumer-001");
    state.ifPresent(s -> {
        log.info("Worker {} - Status: {}", s.workerId(), s.status());
    });
    

    6.4 Événements techniques

    // Logger un événement
    techDb.logEvent("PIPELINE_ERROR", Map.of(
        "pipeline", "order-processing",
        "error", "Connection timeout",
        "messageId", "msg-123"
    ));
    
    // Récupérer les événements récents
    List<TechEvent> errors = techDb.getEvents(
        "PIPELINE_ERROR",
        Instant.now().minus(1, ChronoUnit.HOURS),
        100
    );
    

    7. H2 Console Web (Port 9376)

    Le Socle V4 expose une console H2 dediee sur le port 9376 via le TechDbConsoleWorker.

    Acces

    http://localhost:9376
    

    Informations de connexion

    Champ Valeur
    JDBC URL jdbc:h2:./data/socle-techdb (local) ou jdbc:h2:/app/data/socle-techdb (Docker)
    User LMVI (defaut, personnalisable via TECHDB_USERNAME)
    Password LMVI-SOCLEV004 (defaut, personnalisable via TECHDB_PASSWORD)
    Driver org.h2.Driver

    Configuration

    socle:
      techdb:
        console:
          enabled: true       # Activer/desactiver
          port: 9376          # Port dedie
          bind_address: ""    # Vide = toutes interfaces
          allow_remote: false # Securite: localhost only
    

    Requetes utiles

    -- Voir tous les offsets
    SELECT * FROM techdb_offsets ORDER BY x_dateCreated DESC;
    
    -- Offsets Kafka uniquement
    SELECT * FROM techdb_offsets WHERE topic LIKE 'kafka%';
    
    -- Etat des workers
    SELECT worker_name, state, last_run_at, error_count FROM techdb_worker_state;
    
    -- Evenements non traites
    SELECT * FROM techdb_events WHERE processed = FALSE ORDER BY x_dateCreated;
    
    -- Logs non envoyes
    SELECT COUNT(*) as pending FROM techdb_log_buffer WHERE forwarded = FALSE;
    
    -- Cles KV avec expiration
    SELECT kv_key, value_type, expires_at FROM techdb_kv WHERE expires_at IS NOT NULL;
    

    8. API SQL REST (Nouveaute V4.0.2)

    Le Socle V4 expose une API REST permettant d’executer des requetes SQL sur la TechDB. Cette API est distincte de la console H2 et offre un acces programmatique securise.

    8.1 Configuration

    socle:
      techdb:
        sql_api:
          enabled: ${TECHDB_SQL_API_ENABLED:false}
          auth:
            user: ${TECHDB_API_USER:admin}
            password: ${TECHDB_API_PASSWORD:}
          security:
            readonly: ${TECHDB_SQL_API_READONLY:true}
            blocked_tables: ${TECHDB_SQL_API_BLOCKED_TABLES:}
          limits:
            max_rows: ${TECHDB_SQL_API_MAX_ROWS:1000}
            timeout_seconds: ${TECHDB_SQL_API_TIMEOUT:30}
            rate_limit_per_minute: ${TECHDB_SQL_API_RATE_LIMIT:60}
          logging:
            log_queries: ${TECHDB_SQL_API_LOG_QUERIES:true}
    

    8.2 Variables d’environnement

    Variable Description Defaut
    TECHDB_SQL_API_ENABLED Activer l’API SQL false
    TECHDB_API_USER Utilisateur Basic Auth admin
    TECHDB_API_PASSWORD Mot de passe (vide = pas d’auth) « 
    TECHDB_SQL_API_READONLY Mode lecture seule true
    TECHDB_SQL_API_BLOCKED_TABLES Tables interdites (CSV) « 
    TECHDB_SQL_API_MAX_ROWS Limite de lignes 1000
    TECHDB_SQL_API_TIMEOUT Timeout requetes (sec) 30
    TECHDB_SQL_API_RATE_LIMIT Requetes/minute/IP 60

    8.3 Endpoints

    Methode Path Description
    POST /techdb/query Executer une requete SQL
    GET /techdb/tables Liste des tables
    GET /techdb/tables/{name} Details d’une table
    GET /techdb/stats Statistiques DB

    8.4 Authentification

    L’API utilise Basic Auth. Si un mot de passe est configure, toutes les requetes doivent inclure l’en-tete:

    Authorization: Basic base64(user:password)
    

    Exemple avec curl:

    # Sans authentification (si TECHDB_API_PASSWORD vide)
    curl http://localhost:8080/techdb/tables
    
    # Avec authentification
    curl -u admin:monmotdepasse http://localhost:8080/techdb/query \
      -H "Content-Type: application/json" \
      -d '{"sql": "SELECT * FROM techdb_kv LIMIT 10"}'
    

    8.5 Executer une requete SQL

    Requete:

    curl -X POST http://localhost:8080/techdb/query \
      -H "Content-Type: application/json" \
      -u admin:secret \
      -d '{
        "sql": "SELECT * FROM techdb_events WHERE event_type = ?",
        "params": ["WORKER_START"],
        "maxRows": 100
      }'
    

    Reponse succes:

    {
      "success": true,
      "timestamp": 1705312800000,
      "executionTimeMs": 12,
      "rowCount": 15,
      "columns": ["X_ID", "X_DATECREATED", "EVENT_TYPE", "SOURCE", "DATAS"],
      "rows": [
        {"X_ID": 1, "X_DATECREATED": "2025-01-15T10:00:00", "EVENT_TYPE": "WORKER_START", ...},
        ...
      ],
      "truncated": false
    }
    

    Reponse erreur:

    {
      "success": false,
      "timestamp": 1705312800000,
      "error": {
        "code": "SQL_SYNTAX_ERROR",
        "message": "Syntax error in SQL statement"
      }
    }
    

    8.6 Codes d’erreur

    Code HTTP Status Description
    SQL_SYNTAX_ERROR 400 Erreur de syntaxe SQL
    DDL_NOT_ALLOWED 403 Operation DDL interdite
    READONLY_VIOLATION 403 Non-SELECT en mode readonly
    TABLE_BLOCKED 403 Table bloquee par config
    QUERY_TIMEOUT 408 Timeout depasse
    UNAUTHORIZED 401 Authentification requise
    RATE_LIMITED 429 Limite requetes depassee
    TECHDB_DISABLED 503 TechDB non disponible

    8.7 Securite

    Operations toujours interdites:

    • DDL: DROP, ALTER, CREATE, TRUNCATE, GRANT, REVOKE
    • Commandes dangereuses: SHUTDOWN, SCRIPT, BACKUP, RESTORE

    Mode readonly (defaut):

    • Seuls les SELECT sont autorises
    • INSERT, UPDATE, DELETE interdits

    Tables bloquees:

    socle:
      techdb:
        sql_api:
          security:
            blocked_tables: techdb_log_buffer,techdb_kv  # CSV
    

    8.8 Rate Limiting

    L’API applique un rate limiting par IP (sliding window par minute).

    Par defaut: 60 requetes/minute/IP

    Depassement = HTTP 429 Too Many Requests

    8.9 Metriques Prometheus

    techdb_sql_api_queries_total{status="success|error|timeout"}
    techdb_sql_api_queries_duration_seconds
    techdb_sql_api_auth_failures_total
    techdb_sql_api_rate_limited_total
    

    8.10 Exemples pratiques

    Lister les tables:

    curl http://localhost:8080/techdb/tables
    

    Details d’une table:

    curl http://localhost:8080/techdb/tables/TECHDB_EVENTS
    

    Statistiques DB:

    curl http://localhost:8080/techdb/stats
    

    Requete avec parametres:

    curl -X POST http://localhost:8080/techdb/query \
      -H "Content-Type: application/json" \
      -d '{
        "sql": "SELECT worker_name, state, error_count FROM techdb_worker_state WHERE state = ?",
        "params": ["RUNNING"]
      }'
    

    9. Workers TechDB

    Le Socle V4 inclut 3 workers dedies a la gestion de TechDB:

    9.1 TechDbReaderWorker

    Worker PASSIVE exposant des methodes de lecture.

    @Autowired
    private TechDbReaderWorker reader;
    
    // Recuperer tous les offsets
    List<Map<String, Object>> offsets = reader.getAllOffsets();
    
    // Recuperer les evenements recents
    List<Map<String, Object>> events = reader.getRecentEvents(100);
    
    // Executer une requete personnalisee
    List<Map<String, Object>> results = reader.executeCustomQuery(
        "SELECT * FROM techdb_worker_state WHERE state = 'RUNNING'"
    );
    

    9.2 TechDbPurgeWorker

    Worker CRON qui purge automatiquement les donnees obsoletes.

    socle:
      techdb:
        purge:
          enabled: true
          schedule: "0 0 3 * * ?"  # 3h du matin
          events_retention_days: 7
          logs_retention_days: 3
    

    Donnees purgees :

    • Evenements traites > 7 jours
    • Logs forwardes > 3 jours
    • Cles KV expirees

    9.3 TechDbConsoleWorker

    Worker PASSIVE qui demarre la console H2 sur le port 9376.

    @Autowired
    private TechDbConsoleWorker console;
    
    // Verifier si la console est accessible
    boolean running = console.isConsoleRunning();
    
    // Obtenir les infos de connexion
    Map<String, String> info = console.getConnectionInfo();
    

    10. Fonctions JSON H2

    H2 2.x supporte les fonctions JSON SQL standard :

    -- Extraction de valeur
    SELECT JSON_VALUE('{"name":"John","age":30}', '$.name');
    -- Résultat: John
    
    -- Extraction d'objet
    SELECT JSON_QUERY('{"data":{"items":[1,2,3]}}', '$.data');
    -- Résultat: {"items":[1,2,3]}
    
    -- Test d'existence
    SELECT JSON_EXISTS('{"name":"John"}', '$.name');
    -- Résultat: TRUE
    
    -- Construction JSON
    SELECT JSON_OBJECT('name': 'John', 'age': 30);
    -- Résultat: {"name":"John","age":30}
    
    -- Filtrage sur JSON
    SELECT * FROM socle_events
    WHERE JSON_VALUE(payload, '$.severity') = 'ERROR';
    

    11. Integration avec SharedDataRegistry

    TechDB complète SharedDataRegistry :

    Aspect SharedDataRegistry TechDbManager
    Scope Runtime (mémoire) Persistant (fichier)
    Survie restart Non Oui
    Performance Ultra rapide Rapide
    Usage Métriques live Offsets, état

    Exemple de synergie

    @Service
    public class MonService {
    
        @Autowired
        private TechDbManager techDb;
    
        @Autowired
        private SharedDataRegistry sharedData;
    
        public void initialize() {
            // Restaurer l'offset depuis TechDB
            OptionalLong persisted = techDb.getOffset("kafka", "topic-0");
    
            // Créer le compteur en mémoire
            sharedData.createSequence("kafka.offset.topic-0",
                persisted.orElse(0L),
                HealthLevel.CRITICAL);
        }
    
        public void onMessage(long offset) {
            // Mettre à jour en mémoire (rapide)
            sharedData.setSequence("kafka.offset.topic-0", offset);
    
            // Persister périodiquement (moins fréquent)
            if (offset % 1000 == 0) {
                techDb.saveOffset("kafka", "topic-0", offset, null);
            }
        }
    }
    

    12. Bonnes pratiques

    DO

    • ✅ Utiliser TechDB pour les données qui doivent survivre au restart
    • ✅ Persister les offsets périodiquement (pas à chaque message)
    • ✅ Utiliser JSON pour les métadonnées flexibles
    • ✅ Activer H2 Console uniquement en dev

    DON’T

    • ❌ Stocker des données volumineuses (utiliser PostgreSQL)
    • ❌ Faire des requêtes complexes en boucle doWork()
    • ❌ Activer H2 Console en production
    • ❌ Utiliser pour du cache haute fréquence (utiliser KvBus)

    13. Troubleshooting

    Base corrompue

    # Supprimer et recréer
    rm -rf ./data/socle-techdb.*
    # Redémarrer l'application
    

    Fichier verrouillé

    Database may be already in use: "locked by another process"
    

    Solution : Arrêter l’autre instance ou utiliser AUTO_SERVER=TRUE dans l’URL.

    Console H2 inaccessible

    1. Vérifier socle.techdb.console.enabled: true
    2. Vérifier que l’application tourne
    3. Essayer avec le chemin complet du fichier

    14. References

    Socle V004 – TechDB H2

  • 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