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