service:
  name: logcorrelator
  context: http-network-correlation
  language: go
  pattern: hexagonal
  description: >
    logcorrelator est un service système (lancé par systemd) écrit en Go, chargé
    de recevoir deux flux de logs JSON via des sockets Unix, de corréler les
    événements HTTP applicatifs (source A, typiquement Apache ou reverse proxy)
    avec des événements réseau (source B, métadonnées IP/TCP, JA3/JA4, etc.)
    sur la base de la combinaison strictement définie src_ip + src_port, avec
    une fenêtre temporelle configurable. Le service supporte les connexions
    HTTP Keep-Alive : un log réseau peut être corrélé à plusieurs logs HTTP
    successifs (stratégie 1‑à‑N). La rétention en mémoire est bornée par des
    tailles de caches configurables et un TTL dynamique pour la source B. Le
    service émet toujours les événements A même lorsqu'aucun événement B n'est
    disponible, n'émet jamais de logs B seuls, et pousse les résultats vers
    ClickHouse et/ou un fichier local.
    
    Fonctionnalités de débogage incluses :
    - Serveur de métriques HTTP (/metrics, /health)
    - Logs DEBUG détaillés avec raisons des échecs de corrélation
    - Filtrage des IPs source (exclude_source_ips)
    - Scripts de test (Bash et Python)
    - Métriques : événements reçus, corrélations, échecs par raison, buffers, orphelins

runtime:
  deployment:
    unit_type: systemd
    description: >
      logcorrelator est livré sous forme de binaire autonome, exécuté comme un
      service systemd. L'unité systemd assure le démarrage automatique au boot,
      le redémarrage en cas de crash, et une intégration standard dans l'écosystème
      Linux.
    binary_path: /usr/bin/logcorrelator
    config_path: /etc/logcorrelator/logcorrelator.yml
    user: logcorrelator
    group: logcorrelator
    restart: on-failure
    systemd_unit:
      path: /etc/systemd/system/logcorrelator.service
      content_example: |
        [Unit]
        Description=logcorrelator service
        After=network.target

        [Service]
        Type=simple
        User=logcorrelator
        Group=logcorrelator
        ExecStart=/usr/bin/logcorrelator -config /etc/logcorrelator/logcorrelator.yml
        ExecReload=/bin/kill -HUP $MAINPID
        Restart=on-failure
        RestartSec=5

        # Security hardening
        NoNewPrivileges=true
        ProtectSystem=strict
        ProtectHome=true
        ReadWritePaths=/var/log/logcorrelator /var/run/logcorrelator /etc/logcorrelator

        # Resource limits
        LimitNOFILE=65536

        # Systemd timeouts
        TimeoutStartSec=10
        TimeoutStopSec=30

        [Install]
        WantedBy=multi-user.target
  os:
    supported:
      - rocky-linux-8
      - rocky-linux-9
      - almalinux-10
      - autres-linux-recentes
  logs:
    stdout_stderr: journald
    structured: true
    description: >
      Les logs internes du service (erreurs, messages d'information) sont envoyés
      vers stdout/stderr et collectés par journald. Ils sont structurés et ne
      contiennent pas de données personnelles.
  signals:
    graceful_shutdown:
      - SIGINT
      - SIGTERM
    reload:
      - SIGHUP
    description: >
      SIGINT/SIGTERM : arrêt propre (arrêt des sockets, vidage des buffers, fermeture
      des sinks). SIGHUP : réouverture des fichiers de sortie (utile pour la
      rotation des logs via logrotate) sans arrêter le service.
  filesystem:
    description: >
      Permissions et propriété des fichiers et répertoires utilisés par logcorrelator.
    directories:
      - path: /var/run/logcorrelator
        owner: logcorrelator:logcorrelator
        permissions: "0755"
        purpose: >
          Contient les sockets Unix (http.socket, network.socket).
          Les sockets sont créés avec des permissions 0666 (world read/write).
      - path: /var/log/logcorrelator
        owner: logcorrelator:logcorrelator
        permissions: "0750"
        purpose: >
          Contient les logs corrélés (correlated.log).
      - path: /var/lib/logcorrelator
        owner: logcorrelator:logcorrelator
        permissions: "0750"
        purpose: >
          Répertoire home du service (données internes).
      - path: /etc/logcorrelator
        owner: logcorrelator:logcorrelator
        permissions: "0750"
        purpose: >
          Contient la configuration (logcorrelator.yml, logcorrelator.yml.example).
    files:
      - path: /etc/logcorrelator/logcorrelator.yml
        owner: logcorrelator:logcorrelator
        permissions: "0640"
        rpm_directive: "%config(noreplace)"
      - path: /etc/logcorrelator/logcorrelator.yml.example
        owner: logcorrelator:logcorrelator
        permissions: "0640"
      - path: /etc/systemd/system/logcorrelator.service
        owner: root:root
        permissions: "0644"
      - path: /etc/logrotate.d/logcorrelator
        owner: root:root
        permissions: "0644"
        rpm_directive: "%config(noreplace)"
    sockets:
      - path: /var/run/logcorrelator/http.socket
        owner: logcorrelator:logcorrelator
        permissions: "0666"
        type: unix_datagram
        purpose: "Source A - logs HTTP applicatifs"
      - path: /var/run/logcorrelator/network.socket
        owner: logcorrelator:logcorrelator
        permissions: "0666"
        type: unix_datagram
        purpose: "Source B - logs réseau"

packaging:
  description: >
    logcorrelator est distribué sous forme de packages .rpm (Rocky Linux, AlmaLinux,
    RHEL), construits intégralement dans des conteneurs. Le changelog RPM est mis
    à jour à chaque changement de version.
    Tous les numéros de version doivent être cohérents entre le spec RPM, le Makefile
    (PKG_VERSION), le CHANGELOG.md et les tags git.
    
    Politique de mise à jour de la configuration :
    - Le fichier logcorrelator.yml est marqué %config(noreplace) : il n'est JAMAIS
      écrasé lors d'une mise à jour. La configuration existante est préservée.
    - Le fichier logcorrelator.yml.example est TOUJOURS mis à jour pour refléter
      les nouvelles options de configuration disponibles.
    - Lors de la première installation, si logcorrelator.yml n'existe pas, il est
      créé à partir de logcorrelator.yml.example.
  formats:
    - rpm
  target_distros:
    - rocky-linux-8
    - rocky-linux-9
    - almalinux-10
    - rhel-8
    - rhel-9
    - rhel-10
  rpm:
    tool: fpm
    changelog:
      source: git   # ou CHANGELOG.md
      description: >
        À chaque build, un script génère un fichier de changelog RPM à partir de
        l'historique (tags/commits) et le passe à fpm (option --rpm-changelog).
    contents:
      - path: /usr/bin/logcorrelator
        type: binary
      - path: /etc/logcorrelator/logcorrelator.yml
        type: config
        directives: "%config(noreplace)"
        behavior: >
          Jamais écrasé lors des mises à jour. Préservé automatiquement par RPM.
          Créé uniquement lors de la première installation s'il n'existe pas.
      - path: /etc/logcorrelator/logcorrelator.yml.example
        type: doc
        behavior: >
          TOUJOURS mis à jour lors des mises à jour. Sert de référence pour les
          nouvelles options de configuration disponibles.
      - path: /etc/systemd/system/logcorrelator.service
        type: systemd_unit
      - path: /etc/logrotate.d/logcorrelator
        type: logrotate_script
        directives: "%config(noreplace)"
    logrotate_example: |
      /var/log/logcorrelator/correlated.log {
          daily
          rotate 7
          compress
          delaycompress
          missingok
          notifempty
          create 0640 logcorrelator logcorrelator
          sharedscripts
          postrotate
              /bin/systemctl reload logcorrelator > /dev/null 2>&1 || true
          endscript
      }

config:
  format: yaml
  location: /etc/logcorrelator/logcorrelator.yml
  reload_strategy: signal_sighup_for_files
  description: >
    Toute la configuration est centralisée dans un fichier YAML lisible. Le RPM
    fournit aussi un fichier d'exemple mis à jour à chaque version.
  example: |
    # /etc/logcorrelator/logcorrelator.yml

    log:
      level: INFO  # DEBUG, INFO, WARN, ERROR

    inputs:
      unix_sockets:
        # Source HTTP (A) : logs applicatifs en JSON, 1 datagramme = 1 log.
        - name: http
          source_type: A
          path: /var/run/logcorrelator/http.socket
          format: json
          socket_permissions: "0666"

        # Source réseau (B) : logs IP/TCP/JA3... en JSON, 1 datagramme = 1 log.
        - name: network
          source_type: B
          path: /var/run/logcorrelator/network.socket
          format: json
          socket_permissions: "0666"

    outputs:
      file:
        enabled: true
        path: /var/log/logcorrelator/correlated.log

      clickhouse:
        enabled: false
        dsn: clickhouse://user:pass@localhost:9000/db
        table: correlated_logs_http_network
        batch_size: 500
        flush_interval_ms: 200
        max_buffer_size: 5000
        drop_on_overflow: true
        async_insert: true
        timeout_ms: 1000

      stdout:
        enabled: false
        level: INFO  # DEBUG: tous les logs (y compris orphelins), INFO: seulement corrélés, WARN: corrélés seulement, ERROR: aucun

    correlation:
      # Fenêtre de corrélation : si le log HTTP arrive avant le réseau, il attend
      # au plus cette durée (sauf éviction du cache HTTP).
      # Augmentée à 10s pour supporter le Keep-Alive HTTP.
      time_window:
        value: 10
        unit: s

      orphan_policy:
        apache_always_emit: true  # Toujours émettre les événements A, même sans correspondance B
        network_emit: false       # Ne jamais émettre les événements B seuls

      matching:
        mode: one_to_many  # Keep‑Alive : un B peut corréler plusieurs A.

      buffers:
        # Tailles max des caches en mémoire (en nombre de logs).
        max_http_items: 10000
        max_network_items: 20000

      ttl:
        # Durée de vie standard d'un log réseau (B) en mémoire. Chaque corrélation
        # réussie avec un A réinitialise ce TTL.
        # Augmenté à 120s pour supporter les sessions HTTP Keep-Alive longues.
        network_ttl_s: 120

      # Filtrage des IPs source à exclure (optionnel)
      exclude_source_ips:
        - 10.0.0.1           # IP unique
        - 172.16.0.0/12      # Plage CIDR
        # Les événements depuis ces IPs sont silencieusement ignorés

    # Serveur de métriques HTTP (optionnel, pour débogage et monitoring)
    metrics:
      enabled: false
      addr: ":8080"  # Adresse d'écoute du serveur HTTP
      # Endpoints:
      #   GET /metrics - Retourne les métriques de corrélation en JSON
      #   GET /health  - Health check

inputs:
  description: >
    Deux flux de logs JSON via sockets Unix datagram (SOCK_DGRAM). Chaque datagramme
    contient un JSON complet. Le champ source_type ("A" ou "B") doit être spécifié
    pour chaque socket. À défaut, la source est déduite automatiquement (présence de
    headers = source A, sinon source B).
  unix_sockets:
    - name: http
      id: A
      description: >
        Source A, logs HTTP applicatifs (Apache, reverse proxy, etc.). Schéma JSON
        variable, champ timestamp (int64, nanosecondes) obligatoire, headers dynamiques (header_*).
      path: /var/run/logcorrelator/http.socket
      source_type: A
      permissions: "0666"
      protocol: unix
      socket_type: dgram
      mode: datagram
      format: json
      framing: message
      max_datagram_bytes: 65535
      retry_on_error: true

    - name: network
      id: B
      description: >
        Source B, logs réseau (métadonnées IP/TCP, JA3/JA4, etc.). Seuls src_ip
        et src_port sont requis pour la corrélation. Le champ timestamp est optionnel ;
        s'il est absent, l'heure de réception est utilisée.
      path: /var/run/logcorrelator/network.socket
      source_type: B
      permissions: "0666"
      protocol: unix
      socket_type: dgram
      mode: datagram
      format: json
      framing: message
      max_datagram_bytes: 65535
      retry_on_error: true

outputs:
  description: >
    Les logs corrélés sont envoyés vers un ou plusieurs sinks (MultiSink).
  sinks:
    file:
      enabled: true
      description: >
        Sink fichier local. Un JSON par ligne. Rotation gérée par logrotate,
        réouverture du fichier sur SIGHUP. Le champ `enabled: false` coupe
        completement l'ecriture du fichier (le sink n'est pas cree).
      path: /var/log/logcorrelator/correlated.log
      format: json_lines
      rotate_managed_by: external_logrotate
    clickhouse:
      enabled: false
      description: >
        Sink principal pour l'archivage et l'analyse quasi temps réel. Inserts
        batch asynchrones, drop en cas de saturation. Le service insère uniquement
        dans une table RAW (raw_json String, ingest_time DateTime DEFAULT now()).
        La table parsée et la vue matérialisée sont gérées en externe (DDL séparés).
        Toutes les erreurs de connexion, de flush et de retry sont loggées :
        INFO à la connexion, ERROR sur échec de flush, WARN sur drop/retry, DEBUG sur envoi réussi.
      dsn: clickhouse://user:pass@host:9000/db
      table: correlated_logs_http_network
      batch_size: 500
      flush_interval_ms: 200
      max_buffer_size: 5000
      drop_on_overflow: true
      async_insert: true
      timeout_ms: 1000
    stdout:
      enabled: false
      description: >
        Sink no-op pour les données. Aucune donnée corrélée ou orpheline n'est
        jamais écrite sur stdout. Ce sink existe uniquement pour satisfaire
        l'interface CorrelatedLogSink. Les logs opérationnels du service
        (démarrage, erreurs, métriques de débogage) sont toujours sur stderr
        via observability.Logger, indépendamment de ce sink.

correlation:
  description: >
    Corrélation stricte basée sur src_ip + src_port et une fenêtre temporelle
    configurable. Aucun autre champ n'est utilisé pour la décision de corrélation.
  key:
    - src_ip
    - src_port
  time_window:
    value: 10
    unit: s
    description: >
      Fenêtre de temps appliquée aux timestamps de A et B. Si B n'arrive pas dans
      ce délai, A est émis comme orphelin. Augmentée à 10s pour le Keep-Alive.
  retention_limits:
    max_http_items: 10000
    max_network_items: 20000
    description: >
      Limites des caches. Si max_http_items est atteint, le plus ancien A est
      évincé et émis orphelin. Si max_network_items est atteint, le plus ancien B
      est supprimé silencieusement.
  ttl_management:
    network_ttl_s: 120
    description: >
      TTL des logs réseau. Chaque fois qu'un B est corrélé à un A (Keep-Alive),
      son TTL est remis à cette valeur. Augmenté à 120s pour les sessions longues.
  timestamp_source:
    apache: timestamp (champ int64, nanosecondes)
    network: timestamp (champ int64, nanosecondes) si présent, sinon time (RFC3339),
             sinon reception_time (time.Now())
  orphan_policy:
    apache_always_emit: true
    network_emit: false
  matching:
    mode: one_to_many
    description: >
      Stratégie 1‑à‑N : un log réseau peut être utilisé pour plusieurs logs HTTP
      successifs tant qu'il n'a pas expiré ni été évincé.
  ip_filtering:
    directive: exclude_source_ips
    description: >
      Liste d'IPs source (exactes ou plages CIDR) à ignorer silencieusement.
      Événements non corrélés, non émis en orphelin. Métrique : failed_ip_excluded.
  dest_port_filtering:
    directive: include_dest_ports
    description: >
      Liste blanche de ports de destination. Si non vide, seuls les événements
      dont le dst_port est dans la liste participent à la corrélation. Les autres
      sont silencieusement ignorés (non corrélés, non émis en orphelin).
      Liste vide = tous les ports autorisés (comportement par défaut).
      Métrique : failed_dest_port_filtered.
    example:
      include_dest_ports: [80, 443, 8080, 8443]

schema:
  description: >
    Schémas variables pour A et B. Quelques champs seulement sont obligatoires
    pour la corrélation, les autres sont acceptés sans modification de code.
  source_A:
    description: >
      Logs HTTP applicatifs au format JSON.
    required_fields:
      - name: src_ip
        type: string
      - name: src_port
        type: int
      - name: timestamp
        type: int64
        unit: ns
    optional_fields:
      - name: dst_ip
        type: string
      - name: dst_port
        type: int
      - name: method
        type: string
      - name: path
        type: string
      - name: host
        type: string
      - name: http_version
        type: string
    dynamic_fields:
      - pattern: header_*
        target_map: headers
      - pattern: "*"
        target_map: extra
  source_B:
    description: Logs réseau JSON (IP/TCP, JA3/JA4...).
    required_fields:
      - name: src_ip
        type: string
      - name: src_port
        type: int
    optional_fields:
      - name: dst_ip
        type: string
      - name: dst_port
        type: int
      - name: timestamp
        type: int64
        unit: ns
      - name: time
        type: string
        format: RFC3339 ou RFC3339Nano
    dynamic_fields:
      - pattern: "*"
        target_map: extra

  normalized_event:
    description: >
      Représentation interne unifiée des événements A/B.
    fields:
      - name: source
        type: enum("A","B")
      - name: timestamp
        type: time.Time
      - name: src_ip
        type: string
      - name: src_port
        type: int
      - name: dst_ip
        type: string
        optional: true
      - name: dst_port
        type: int
        optional: true
      - name: headers
        type: map[string]string
        optional: true
      - name: extra
        type: map[string]any

  correlated_log:
    description: >
      Structure du log corrélé émis vers les sinks.
    fields:
      - name: timestamp
        type: time.Time
      - name: src_ip
        type: string
      - name: src_port
        type: int
      - name: dst_ip
        type: string
        optional: true
      - name: dst_port
        type: int
        optional: true
      - name: correlated
        type: bool
      - name: orphan_side
        type: string
      - name: "*"
        type: map[string]any

clickhouse_schema:
  strategy: external_ddls
  database: ja4_processing
  description: >
    La table ClickHouse est gérée en dehors du service. Le service insère dans une
    table RAW avec une seule colonne raw_json contenant le log corrélé complet
    sérialisé en JSON. La colonne ingest_time utilise DEFAULT now().
    Toute extraction de champs (table parsée, vue matérialisée) est gérée en externe
    via des DDL séparés, non implémentés dans le service.
  tables:
    - name: http_logs_raw
      description: >
        Table d'ingestion brute. Une seule colonne raw_json contient le log corrélé
        complet sérialisé en JSON. La colonne ingest_time est auto-générée avec
        DEFAULT now(). Partitionnée par jour pour optimiser le TTL.
      engine: MergeTree
      partition_by: toDate(ingest_time)
      order_by: ingest_time
      columns:
        - name: raw_json
          type: String
        - name: ingest_time
          type: DateTime
          default: now()
      insert_format: |
        INSERT INTO ja4_processing.http_logs_raw (raw_json) VALUES
        ('{...log corrélé sérialisé en JSON...}')
      notes: >
        Le service utilise l'API native clickhouse-go/v2 (PrepareBatch + Append + Send).
        La colonne ingest_time n'est PAS explicitement insérée (DEFAULT now() est utilisé).

    - name: http_logs
      description: >
        Table parsée (optionnelle, gérée en externe). Le service n'implémente PAS
        l'extraction des champs suivants. Si cette table est utilisée, elle doit être
        alimentée par une vue matérialisée ou un traitement ETL externe.
      engine: MergeTree
      partition_by: log_date
      order_by: (time, src_ip, dst_ip, ja4)
      columns:
        - name: time
          type: DateTime
        - name: log_date
          type: Date
          default: toDate(time)
        - name: src_ip
          type: IPv4
        - name: src_port
          type: UInt16
        - name: dst_ip
          type: IPv4
        - name: dst_port
          type: UInt16
        - name: method
          type: LowCardinality(String)
        - name: scheme
          type: LowCardinality(String)
        - name: host
          type: LowCardinality(String)
        - name: path
          type: String
        - name: query
          type: String
        - name: http_version
          type: LowCardinality(String)
        - name: orphan_side
          type: LowCardinality(String)
        - name: correlated
          type: UInt8
        - name: keepalives
          type: UInt16
          status: non_implémenté
        - name: a_timestamp
          type: UInt64
          status: non_implémenté
        - name: b_timestamp
          type: UInt64
          status: non_implémenté
        - name: conn_id
          type: String
          status: non_implémenté
        - name: ip_meta_df
          type: UInt8
          status: non_implémenté
        - name: ip_meta_id
          type: UInt32
          status: non_implémenté
        - name: ip_meta_total_length
          type: UInt32
          status: non_implémenté
        - name: ip_meta_ttl
          type: UInt8
          status: non_implémenté
        - name: tcp_meta_options
          type: LowCardinality(String)
          status: non_implémenté
        - name: tcp_meta_window_size
          type: UInt32
          status: non_implémenté
        - name: syn_to_clienthello_ms
          type: Int32
          status: non_implémenté
        - name: tls_version
          type: LowCardinality(String)
          status: non_implémenté
        - name: tls_sni
          type: LowCardinality(String)
          status: non_implémenté
        - name: ja3
          type: String
          status: non_implémenté
        - name: ja3_hash
          type: String
          status: non_implémenté
        - name: ja4
          type: String
          status: non_implémenté
        - name: header_user_agent
          type: String
          status: non_implémenté
        - name: header_accept
          type: String
          status: non_implémenté
        - name: header_accept_encoding
          type: String
          status: non_implémenté
        - name: header_accept_language
          type: String
          status: non_implémenté
        - name: header_x_request_id
          type: String
          status: non_implémenté
        - name: header_x_trace_id
          type: String
          status: non_implémenté
        - name: header_x_forwarded_for
          type: String
          status: non_implémenté
        - name: header_sec_ch_ua
          type: String
          status: non_implémenté
        - name: header_sec_ch_ua_mobile
          type: String
          status: non_implémenté
        - name: header_sec_ch_ua_platform
          type: String
          status: non_implémenté
        - name: header_sec_fetch_dest
          type: String
          status: non_implémenté
        - name: header_sec_fetch_mode
          type: String
          status: non_implémenté
        - name: header_sec_fetch_site
          type: String
          status: non_implémenté
      notes: >
        Cette table et la vue matérialisée associée sont gérées en externe (DDL séparés).
        Le service se contente d'insérer le JSON brut dans http_logs_raw.
        Les champs marqués "non_implémenté" ne sont PAS extraits par le service.

  users:
    description: >
      La gestion des utilisateurs ClickHouse est externe au service. Le DSN est
      configuré dans le fichier de configuration YAML.
    notes: >
      Cette section est fournie à titre indicatif pour l'administration ClickHouse.

  migration:
    description: >
      Aucune migration n'est implémentée dans le service. La gestion des schémas
      (tables, vues matérialisées) est entièrement externe (DDL séparés).

architecture:
  description: >
    Architecture hexagonale : domaine de corrélation indépendant, ports abstraits
    pour les sources/sinks, adaptateurs pour sockets Unix, fichier, ClickHouse et
    stdout, couche application d'orchestration, et modules infra (config, observabilité).
  modules:
    - name: cmd/logcorrelator
      type: entrypoint
      responsibilities:
        - Chargement de la configuration YAML.
        - Initialisation des adaptateurs d'entrée/sortie.
        - Création du CorrelationService.
        - Démarrage de l'orchestrateur.
        - Gestion des signaux (SIGINT, SIGTERM, SIGHUP).
        - Versioning via -ldflags (main.Version).
    - name: internal/domain
      type: domain
      responsibilities:
        - Modèles NormalizedEvent et CorrelatedLog.
        - CorrelationService (fenêtre, TTL, buffers bornés, one-to-many/Keep-Alive, orphelins).
        - Filtrage par IP source (exclude_source_ips, CIDR).
        - Filtrage par port destination (include_dest_ports, liste blanche).
        - Custom JSON marshaling pour CorrelatedLog (structure plate).
    - name: internal/ports
      type: ports
      responsibilities:
        - Interfaces EventSource, CorrelatedLogSink, CorrelationProcessor.
    - name: internal/app
      type: application
      responsibilities:
        - Orchestrator : EventSource → CorrelationService → MultiSink.
        - Gestion du contexte de shutdown et drain des événements.
    - name: internal/adapters/inbound/unixsocket
      type: adapter_inbound
      responsibilities:
        - Lecture Unix datagram (SOCK_DGRAM) et parsing JSON → NormalizedEvent.
        - Détection automatique de la source (A/B) via source_type ou headers.
        - Gestion des permissions de socket (défaut 0666).
        - Cleanup du fichier socket à l'arrêt.
    - name: internal/adapters/outbound/file
      type: adapter_outbound
      responsibilities:
        - Écriture JSON lines.
        - Réouverture du fichier sur SIGHUP (log rotation).
        - Validation des chemins (répertoire autorisé).
    - name: internal/adapters/outbound/clickhouse
      type: adapter_outbound
      responsibilities:
        - Bufferisation + inserts batch asynchrones.
        - Gestion du drop_on_overflow.
        - Retry avec backoff exponentiel (MaxRetries=3).
        - API native clickhouse-go/v2 (PrepareBatch + Append + Send).
        - Logging complet via observability.Logger (SetLogger) : INFO à la connexion,
          DEBUG sur envoi réussi (rows/table), WARN sur drop buffer et retries,
          ERROR sur échec de flush (périodique, batch, fermeture).
    - name: internal/adapters/outbound/stdout
      type: adapter_outbound
      responsibilities:
        - Sink no-op pour les données corrélées.
        - Write/Flush/Close ne font rien : les données ne passent jamais par stdout.
        - Les logs opérationnels sont sur stderr via observability.Logger (indépendant de ce sink).
    - name: internal/adapters/outbound/multi
      type: adapter_outbound
      responsibilities:
        - Fan-out vers plusieurs sinks.
        - Implémentation de Reopen() pour la rotation des logs.
    - name: internal/config
      type: infrastructure
      responsibilities:
        - Chargement/validation de la configuration YAML.
        - Valeurs par défaut et fallback pour champs dépréciés.
    - name: internal/observability
      type: infrastructure
      responsibilities:
        - Logger structuré avec niveaux (DEBUG, INFO, WARN, ERROR).
        - CorrelationMetrics : suivi des statistiques de corrélation.
        - MetricsServer : serveur HTTP pour exposition des métriques (/metrics, /health).
        - Traçage des événements exclus (exclude_source_ips).
        - Logs pour : événements reçus, corrélations, orphelins, buffer plein.

testing:
  unit:
    description: >
      Tests unitaires table‑driven, couverture cible ≥ 80 %. La couverture actuelle
      est d'environ 74-80% selon les versions. Les tests se concentrent sur la logique
      de corrélation, les caches, les sinks et le parsing des datagrammes.
    coverage_minimum: 0.8
    coverage_actual: ~0.74-0.80
    focus:
      - CorrelationService (fenêtre, TTL, évictions, one-to-many/Keep-Alive)
      - Parsing A/B → NormalizedEvent (datagrammes JSON)
      - ClickHouseSink (batching, retry, overflow, logging erreurs/succès)
      - FileSink (réouverture sur SIGHUP)
      - MultiSink (fan-out)
      - StdoutSink (no-op data, test stdout reste vide)
      - Config (validation, valeurs par défaut, exclude_source_ips)
      - UnixSocketSource (lecture, permissions, cleanup)
      - CorrelationMetrics (suivi des statistiques)
      - MetricsServer (endpoints /metrics et /health)
  integration:
    description: >
      Tests d'intégration limités. Le flux complet A+B → corrélation → sinks est
      testé via des tests unitaires avec mocks. ClickHouse est mocké (pas de tests
      avec vrai ClickHouse). Scénarios Keep-Alive testés dans correlation_service_test.go.
      Scripts de test fournis : scripts/test-correlation.sh et scripts/test-correlation-advanced.py.

docker:
  description: >
    Build, tests et packaging RPM sont exécutés intégralement dans des conteneurs
    via un multi‑stage build. Deux Dockerfiles : Dockerfile (build + runtime + dev)
    et Dockerfile.package (RPM multi-distros : el8, el9, el10).
  build_pipeline:
    multi_stage: true
    stages:
      - name: builder
        base: golang:1.21
        description: >
          go test -race -coverprofile=coverage.txt ./... avec vérification de couverture
          (échec si < 80 %). Compilation d'un binaire statique (CGO_ENABLED=0,
          GOOS=linux, GOARCH=amd64).
      - name: runtime
        base: scratch
        description: >
          Image minimale contenant uniquement le binaire et la config exemple.
      - name: rpm_builder_el8
        base: rockylinux:8
        description: >
          Installation de fpm (via Ruby), construction RPM pour Enterprise Linux 8.
      - name: rpm_builder_el9
        base: rockylinux:9
        description: >
          Installation de fpm (via Ruby), construction RPM pour Enterprise Linux 9.
      - name: rpm_builder_el10
        base: almalinux:10
        description: >
          Installation de fpm (via Ruby), construction RPM pour Enterprise Linux 10.
      - name: output_export
        base: alpine:latest
        description: >
          Export des paquets RPM produits pour les 3 distributions (el8, el9, el10).
  files:
    - path: Dockerfile
      description: Build principal (builder, runtime, dev) et packaging RPM mono-distro.
    - path: Dockerfile.package
      description: Packaging RPM multi-distros (el8, el9, el10) avec scripts post/preun/postun.

observability:
  description: >
    Le service inclut des fonctionnalités complètes de débogage et de monitoring
    pour diagnostiquer les problèmes de corrélation et surveiller les performances.
  logging:
    levels:
      - DEBUG: Tous les événements reçus, tentatives de corrélation, raisons des échecs
      - INFO: Événements corrélés, démarrage/arrêt du service
      - WARN: Orphelins émis, buffer plein, TTL expiré
      - ERROR: Erreurs de parsing, échecs de sink, erreurs critiques
    debug_logs:
      - "event received: source=A src_ip=192.168.1.1 src_port=8080 timestamp=..."
      - "processing A event: key=192.168.1.1:8080 timestamp=..."
      - "correlation found: A(src_ip=... src_port=... ts=...) + B(src_ip=... src_port=... ts=...)"
      - "A event has no matching B key in buffer: key=..."
      - "A event has same key as B but outside time window: key=... time_diff=5s window=10s"
      - "event excluded by IP filter: source=A src_ip=10.0.0.1 src_port=8080"
      - "event excluded by dest port filter: source=A dst_port=22"
      - "TTL reset for B event (Keep-Alive): key=... new_ttl=120s"
      - "[clickhouse] DEBUG batch sent: rows=42 table=correlated_logs_http_network"
    info_logs:
      - "[clickhouse] INFO connected to ClickHouse: table=... batch_size=500 flush_interval_ms=200"
    warn_logs:
      - "[clickhouse] WARN buffer full, dropping log: table=... buffer_size=5000"
      - "[clickhouse] WARN retrying batch insert: attempt=2/3 delay=100ms rows=42 err=connection refused"
    error_logs:
      - "[clickhouse] ERROR periodic flush failed: ..."
      - "[clickhouse] ERROR batch flush failed: ..."
      - "[clickhouse] ERROR final flush on close failed: ..."
  metrics_server:
    enabled: true
    endpoints:
      - path: /metrics
        method: GET
        description: Retourne les métriques de corrélation au format JSON
        response_example: |
          {
            "events_received_a": 1542,
            "events_received_b": 1498,
            "correlations_success": 1450,
            "correlations_failed": 92,
            "failed_no_match_key": 45,
            "failed_time_window": 23,
            "failed_buffer_eviction": 5,
            "failed_ttl_expired": 12,
            "failed_ip_excluded": 7,
            "failed_dest_port_filtered": 3,
            "buffer_a_size": 23,
            "buffer_b_size": 18,
            "orphans_emitted_a": 92,
            "keepalive_resets": 892
          }
      - path: /health
        method: GET
        description: Health check
        response_example: |
          {"status":"healthy"}
  metrics_tracked:
    events_received:
      - events_received_a: Nombre d'événements HTTP (source A) reçus
      - events_received_b: Nombre d'événements réseau (source B) reçus
    correlations:
      - correlations_success: Corrélations réussies
      - correlations_failed: Échecs de corrélation
    failure_reasons:
      - failed_no_match_key: Clé src_ip:src_port non trouvée dans le buffer
      - failed_time_window: Événements hors fenêtre temporelle
      - failed_buffer_eviction: Buffer plein, événement évincé
      - failed_ttl_expired: TTL du événement B expiré
      - failed_ip_excluded: Événement exclu par filtre IP (exclude_source_ips)
      - failed_dest_port_filtered: Événement exclu par filtre port destination (include_dest_ports)
    buffers:
      - buffer_a_size: Taille actuelle du buffer HTTP
      - buffer_b_size: Taille actuelle du buffer réseau
    orphans:
      - orphans_emitted_a: Orphelins A émis (sans correspondance B)
      - orphans_emitted_b: Orphelins B émis (toujours 0, policy: network_emit=false)
      - orphans_pending_a: Orphelins A en attente (délai avant émission)
      - pending_orphan_match: B a corrélé avec un orphelin A en attente
    keepalive:
      - keepalive_resets: Resets TTL pour mode Keep-Alive (one-to-many)
  troubleshooting:
    description: >
      Guide de diagnostic basé sur les métriques et logs
    common_issues:
      - symptom: failed_no_match_key élevé
        cause: Les logs A et B n'ont pas le même src_ip + src_port
        solution: Vérifier que les deux sources utilisent la même combinaison IP/port
      - symptom: failed_time_window élevé
        cause: Timestamps trop éloignés (> time_window.value)
        solution: Augmenter correlation.time_window.value ou synchroniser les horloges (NTP)
      - symptom: failed_ttl_expired élevé
        cause: Les événements B expirent avant corrélation
        solution: Augmenter correlation.ttl.network_ttl_s
      - symptom: failed_buffer_eviction élevé
        cause: Buffers trop petits pour le volume de logs
        solution: Augmenter correlation.buffers.max_http_items et max_network_items
      - symptom: failed_ip_excluded élevé
        cause: Traffic depuis des IPs configurées dans exclude_source_ips
        solution: Vérifier la configuration, c'est normal si attendu
      - symptom: failed_dest_port_filtered élevé
        cause: Traffic sur des ports non listés dans include_dest_ports
        solution: Vérifier la configuration include_dest_ports, ou vider la liste pour tout accepter
      - symptom: orphans_emitted_a élevé
        cause: Beaucoup de logs A sans correspondance B
        solution: Vérifier que la source B envoie bien les événements attendus
  test_scripts:
    - name: scripts/test-correlation.sh
      description: Script Bash pour tester la corrélation avec des événements synthétiques
      features:
        - Envoi de paires A+B avec mêmes src_ip:src_port
        - Vérification des métriques avant/après
        - Options: -c (count), -d (delay), -v (verbose), -m (metrics-url)
    - name: scripts/test-correlation-advanced.py
      description: Script Python avancé avec multiples scénarios de test
      features:
        - Basic test: corrélations simples
        - Time window test: vérifie l'expiration de la fenêtre temporelle
        - Different IP test: vérifie non-corrélation avec IPs différentes
        - Keep-Alive test: vérifie le mode one-to-many
        - Métriques en temps réel

