feat: Keep-Alive correlation, TTL management, SIGHUP handling, logrotate support
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build (push) Has been cancelled
Build and Test / docker (push) Has been cancelled

Major features:
- One-to-many correlation mode (Keep-Alive) for HTTP connections
- Dynamic TTL for network events with reset on each correlation
- Separate configurable buffer sizes for HTTP and network events
- SIGHUP signal handling for log rotation without service restart
- FileSink.Reopen() method for log file rotation
- logrotate configuration included in RPM
- ExecReload added to systemd service

Configuration changes:
- New YAML structure with nested sections (time_window, orphan_policy, matching, buffers, ttl)
- Backward compatibility maintained for deprecated fields

Packaging:
- RPM version 1.1.0 with logrotate config
- Updated spec file and changelog
- All distributions: el8, el9, el10

Tests:
- New tests for Keep-Alive mode and TTL reset
- Updated mocks with Reopen() interface method

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-03-02 20:32:59 +01:00
parent a415a3201a
commit 33e19b4f52
19 changed files with 974 additions and 321 deletions

View File

@ -9,12 +9,13 @@ service:
é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 produit un log corrélé
unique pour chaque paire correspondante, émet toujours les événements A
même lorsquaucun événement B corrélé nest disponible, német jamais de
logs B seuls, et pousse les logs agrégés en temps quasi réel vers
ClickHouse et/ou un fichier local, en minimisant la rétention en mémoire
et sur disque.
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 lorsquaucun événement B nest
disponible, német jamais de logs B seuls, et pousse les résultats vers
ClickHouse et/ou un fichier local.
runtime:
deployment:
@ -23,7 +24,7 @@ runtime:
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 (notamment sur CentOS 7 et Rocky Linux 8+).
Linux.
binary_path: /usr/bin/logcorrelator
config_path: /etc/logcorrelator/logcorrelator.yml
user: logcorrelator
@ -41,6 +42,7 @@ runtime:
User=logcorrelator
Group=logcorrelator
ExecStart=/usr/bin/logcorrelator -config /etc/logcorrelator/logcorrelator.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
@ -63,98 +65,186 @@ runtime:
graceful_shutdown:
- SIGINT
- SIGTERM
reload:
- SIGHUP
description: >
En réception de SIGINT ou SIGTERM, le service arrête proprement la lecture
des sockets Unix, vide les buffers denvoi (dans les limites de la politique
de drop), ferme les connexions ClickHouse puis sarrête.
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.
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.
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
lhistorique (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)"
- path: /etc/logcorrelator/logcorrelator.yml.example
type: doc
description: Fichier d'exemple toujours mis à jour par le RPM.
- path: /etc/systemd/system/logcorrelator.service
type: systemd_unit
- path: /etc/logrotate.d/logcorrelator
type: logrotate_script
logrotate_example: |
/var/log/logcorrelator/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 logcorrelator logcorrelator
postrotate
systemctl reload logcorrelator > /dev/null 2>/dev/null || 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,
stocké dans /etc/logcorrelator.
reload_strategy: restart_service
Toute la configuration est centralisée dans un fichier YAML lisible. Le RPM
fournit aussi un fichier dexemple mis à jour à chaque version.
example: |
# Logging configuration
# /etc/logcorrelator/logcorrelator.yml
log:
level: INFO # DEBUG, INFO, WARN, ERROR
# Inputs - at least 2 unix sockets required
inputs:
unix_sockets:
# Source HTTP (A) : logs applicatifs en JSON, 1 datagramme = 1 log.
- name: http
path: /var/run/logcorrelator/http.socket
socket_permissions: "0660"
- name: network
path: /var/run/logcorrelator/network.socket
path: /var/run/logcorrelator/http.sock
socket_permissions: "0666"
socket_type: dgram
max_datagram_bytes: 65535
# Source réseau (B) : logs IP/TCP/JA3... en JSON, 1 datagramme = 1 log.
- name: network
path: /var/run/logcorrelator/network.sock
socket_permissions: "0666"
socket_type: dgram
max_datagram_bytes: 65535
# Outputs
outputs:
file:
enabled: true
path: /var/log/logcorrelator/correlated.log
clickhouse:
dsn: clickhouse://user:pass@localhost:9000/db
table: correlated_logs
stdout: false
format: json_lines
clickhouse:
enabled: true
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
# Correlation (optional)
correlation:
time_window_s: 1
emit_orphans: true
# 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).
time_window:
value: 1
unit: s
orphan_policy:
apache_always_emit: true
network_emit: false
matching:
mode: one_to_many # KeepAlive : 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 dun log réseau (B) en mémoire. Chaque corrélation
# réussie avec un A réinitialise ce TTL.
network_ttl_s: 30
inputs:
description: >
Le service consomme deux flux de logs JSON via des sockets Unix. Le schéma
exact des logs pour chaque source est flexible et peut évoluer. Seuls
quelques champs sont nécessaires pour la corrélation.
Deux flux de logs JSON via sockets Unix datagram (SOCK_DGRAM). Chaque datagramme
contient un JSON complet.
unix_sockets:
- name: apache_source
id: A
description: >
Source A, destinée aux logs HTTP applicatifs (Apache, reverse proxy, etc.).
Le schéma JSON est variable, avec un champ timestamp numérique obligatoire
et des champs header_* dynamiques.
Source A, logs HTTP applicatifs (Apache, reverse proxy, etc.). Schéma JSON
variable, champ timestamp obligatoire, headers dynamiques (header_*).
path: /var/run/logcorrelator/apache.sock
permissions: "0666"
protocol: unix
mode: stream
socket_type: dgram
mode: datagram
format: json
framing: line
framing: message
max_datagram_bytes: 65535
retry_on_error: true
- name: network_source
id: B
description: >
Source B, destinée aux logs réseau (métadonnées IP/TCP, JA3/JA4, etc.).
Le schéma JSON est variable ; seuls src_ip et src_port sont requis.
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.
path: /var/run/logcorrelator/network.sock
permissions: "0666"
protocol: unix
mode: stream
socket_type: dgram
mode: datagram
format: json
framing: line
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 permet
de diffuser chaque log corrélé vers plusieurs destinations (fichier,
ClickHouse, stdout…).
Les logs corrélés sont envoyés vers un ou plusieurs sinks (MultiSink).
sinks:
file:
enabled: true
description: >
Sink vers fichier local, utile pour debug ou archivage local. Écrit un
JSON par ligne dans le chemin configuré. Rotation gérée par logrotate
ou équivalent.
Sink fichier local. Un JSON par ligne. Rotation gérée par logrotate,
réouverture du fichier sur SIGHUP.
path: /var/log/logcorrelator/correlated.log
format: json_lines
rotate_managed_by: external
rotate_managed_by: external_logrotate
clickhouse:
enabled: true
description: >
Sink principal pour larchivage et lanalyse en temps quasi réel. Les
logs corrélés sont insérés en batch dans ClickHouse avec un small buffer
et des inserts asynchrones. En cas de saturation ou dindisponibilité
ClickHouse, les logs sont drop pour éviter de saturer la machine locale.
Sink principal pour larchivage et lanalyse quasi temps réel. Inserts
batch asynchrones, drop en cas de saturation.
dsn: clickhouse://user:pass@host:9000/db
table: correlated_logs_http_network
batch_size: 500
@ -166,13 +256,12 @@ outputs:
stdout:
enabled: false
description: >
Sink optionnel vers stdout pour les tests et le développement.
Sink optionnel pour les tests/développement.
correlation:
description: >
Corrélation strictement basée sur src_ip + src_port et une fenêtre temporelle
configurable. Aucun autre champ (dst_ip, dst_port, JA3/JA4, headers HTTP...)
nest utilisé pour la décision de corrélation.
Corrélation stricte basée sur src_ip + src_port et une fenêtre temporelle
configurable. Aucun autre champ nest utilisé pour la décision de corrélation.
key:
- src_ip
- src_port
@ -180,53 +269,50 @@ correlation:
value: 1
unit: s
description: >
Fenêtre de temps symétrique appliquée aux timestamps de A et B. Deux
événements sont corrélés si |tA - tB| <= time_window. La valeur et l'unité
sont définies dans le YAML.
Fenêtre de temps appliquée aux timestamps de A et B. Si B narrive pas dans
ce délai, A est émis comme orphelin.
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: 30
description: >
TTL des logs réseau. Chaque fois quun B est corrélé à un A (KeepAlive),
son TTL est remis à cette valeur.
timestamp_source:
apache: field_timestamp
network: reception_time
description: >
Pour A, utilisation du champ numérique "timestamp" (epoch ns). Pour B,
utilisation du temps de réception local.
orphan_policy:
apache_always_emit: true
network_emit: false
description: >
A est toujours émis (même sans B) avec correlated=false et orphan_side="A".
B nest jamais émis seul.
matching:
mode: one_to_one_first_match
mode: one_to_many
description: >
Stratégie 1à1, premier match : lors de larrivée dun événement, on
cherche le premier événement compatible dans le buffer de lautre source.
Les autres restent en attente ou expirent.
Stratégie 1àN : un log réseau peut être utilisé pour plusieurs logs HTTP
successifs tant quil na pas expiré ni été évincé.
schema:
description: >
Les schémas des sources A et B sont variables. Le service impose seulement
quelques champs obligatoires nécessaires à la corrélation et accepte des
champs supplémentaires sans modification de code.
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 (Apache/reverse proxy) au format JSON. Schéma
variable, avec champs obligatoires pour corrélation (src_ip, src_port,
timestamp) et collecte des autres champs dans des maps.
Logs HTTP applicatifs au format JSON.
required_fields:
- name: src_ip
type: string
description: Adresse IP source client.
- name: src_port
type: int
description: Port source client.
- name: timestamp
type: int64
unit: ns
description: Timestamp de référence pour la corrélation.
optional_fields:
- name: time
type: string
format: rfc3339
- name: dst_ip
type: string
- name: dst_port
@ -242,16 +328,10 @@ schema:
dynamic_fields:
- pattern: header_*
target_map: headers
description: >
Tous les champs header_* sont collectés dans headers[clé] = valeur.
- pattern: "*"
target_map: extra
description: >
Tous les champs non reconnus explicitement vont dans extra.
source_B:
description: >
Logs réseau JSON (IP/TCP, JA3/JA4...). Schéma variable. src_ip et src_port
sont obligatoires pour la corrélation, le reste est libre.
description: Logs réseau JSON (IP/TCP, JA3/JA4...).
required_fields:
- name: src_ip
type: string
@ -265,14 +345,10 @@ schema:
dynamic_fields:
- pattern: "*"
target_map: extra
description: >
Tous les autres champs (ip_meta_*, tcp_meta_*, ja3, ja4, etc.) sont
rangés dans extra.
normalized_event:
description: >
Représentation interne unifiée des événements A/B sur laquelle opère la
logique de corrélation.
Représentation interne unifiée des événements A/B.
fields:
- name: source
type: enum("A","B")
@ -293,13 +369,10 @@ schema:
optional: true
- name: extra
type: map[string]any
description: Champs additionnels provenant de A ou B.
correlated_log:
description: >
Structure du log corrélé émis vers les sinks (fichier, ClickHouse). Contient
les informations de corrélation et tous les champs des sources A et B fusionnés
dans une structure JSON plate (flat).
Structure du log corrélé émis vers les sinks.
fields:
- name: timestamp
type: time.Time
@ -319,18 +392,13 @@ schema:
type: string
- name: "*"
type: map[string]any
description: >
Tous les champs additionnels provenant de A et B sont fusionnés
directement à la racine du JSON (structure plate, sans subdivisions).
clickhouse_schema:
strategy: external_ddls
description: >
logcorrelator ne gère pas les ALTER TABLE. La table ClickHouse doit être
créée/modifiée en dehors du service. logcorrelator remplit les colonnes
existantes qu'il connaît et met NULL si un champ manque.
Depuis la version 1.0.3, les champs apache et network sont remplacés par
une colonne unique fields JSON contenant tous les champs fusionnés.
La table ClickHouse est gérée en dehors du service. logcorrelator remplit
les colonnes connues et met NULL si un champ manque. Tous les champs fusionnés
sont exposés dans une colonne JSON (fields).
base_columns:
- name: timestamp
type: DateTime64(9)
@ -350,31 +418,26 @@ clickhouse_schema:
type: JSON
dynamic_fields:
mode: map_or_additional_columns
description: >
Les champs dynamiques peuvent être exposés via colonnes dédiées créées par
migration, ou via Map/JSON.
architecture:
description: >
Architecture hexagonale : domaine de corrélation indépendant, ports
abstraits pour les sources/sinks, adaptateurs pour sockets Unix, fichier et
ClickHouse, couche application dorchestration, et modules infra pour
config/observabilité.
Architecture hexagonale : domaine de corrélation indépendant, ports abstraits
pour les sources/sinks, adaptateurs pour sockets Unix, fichier et ClickHouse,
couche application dorchestration, et modules infra (config, observabilité).
modules:
- name: cmd/logcorrelator
type: entrypoint
responsibilities:
- Chargement configuration YAML.
- Chargement de la configuration YAML.
- Initialisation des adaptateurs d'entrée/sortie.
- Création du CorrelationService.
- Démarrage de l'orchestrateur.
- Gestion du cycle de vie (signaux systemd).
- Démarrage de lorchestrateur.
- Gestion des signaux (SIGINT, SIGTERM, SIGHUP).
- name: internal/domain
type: domain
responsibilities:
- Modèles NormalizedEvent et CorrelatedLog.
- Implémentation de CorrelationService (buffers, fenêtre,
orphelins).
- CorrelationService (fenêtre, TTL, buffers bornés, 1àN, orphelins).
- name: internal/ports
type: ports
responsibilities:
@ -382,163 +445,70 @@ architecture:
- name: internal/app
type: application
responsibilities:
- Orchestrator : relier EventSource → CorrelationService → MultiSink.
- Orchestrator : EventSource → CorrelationService → MultiSink.
- name: internal/adapters/inbound/unixsocket
type: adapter_inbound
responsibilities:
- Lecture sockets Unix + parsing JSON → NormalizedEvent.
- Lecture Unix datagram (SOCK_DGRAM) et parsing JSON → NormalizedEvent.
- name: internal/adapters/outbound/file
type: adapter_outbound
responsibilities:
- Écriture fichier JSON lines.
- Écriture JSON lines.
- Réouverture du fichier sur SIGHUP.
- name: internal/adapters/outbound/clickhouse
type: adapter_outbound
responsibilities:
- Bufferisation + inserts batch vers ClickHouse.
- Application de drop_on_overflow.
- Bufferisation + inserts batch, gestion du drop_on_overflow.
- name: internal/adapters/outbound/multi
type: adapter_outbound
responsibilities:
- Fan-out vers plusieurs sinks.
- Fanout vers plusieurs sinks.
- name: internal/config
type: infrastructure
responsibilities:
- Chargement/validation config YAML.
- Chargement/validation de la configuration YAML.
- name: internal/observability
type: infrastructure
responsibilities:
- Logging et métriques internes.
- Logging interne, métriques (tailles des caches, évictions, erreurs datagram).
testing:
unit:
description: >
Tests unitaires table-driven avec couverture cible ≥ 80 %. Focalisés sur
la logique de corrélation, parsing et sink ClickHouse.[web:94][web:98][web:102]
Tests unitaires tabledriven, couverture cible ≥ 80 %, focale sur la logique
de corrélation, les caches et les sinks.
coverage_minimum: 0.8
focus:
- CorrelationService
- Parsing A/B → NormalizedEvent
- CorrelationService (fenêtre, TTL, évictions, 1àN)
- Parsing A/B → NormalizedEvent (datagrammes)
- ClickHouseSink (batching, overflow)
- FileSink (réouverture sur SIGHUP)
- MultiSink
integration:
description: >
Tests dintégration validant le flux complet A+B → corrélation → sinks,
avec sockets simulés et ClickHouse mocké.
avec sockets Unix datagram simulées, ClickHouse mocké et scénarios KeepAlive.
docker:
description: >
Build et tests entièrement encapsulés dans Docker, avec multistage build :
un stage builder pour compiler et tester, un stage runtime minimal pour
exécuter le service.[web:95][web:103]
images:
builder:
base: golang:latest
purpose: build_and_test
runtime:
base: scratch
purpose: run_binary_only
build:
multi_stage: true
steps:
- name: unit_tests
description: >
go test ./... avec génération de couverture. Le build échoue si la
couverture est < 80 %.
- name: compile_binary
description: >
Compilation CGO_ENABLED=0, GOOS=linux, GOARCH=amd64 pour un binaire
statique /usr/bin/logcorrelator.
- name: assemble_runtime_image
description: >
Copie du binaire dans limage runtime et définition de lENTRYPOINT.
packaging:
description: >
logcorrelator est distribué sous forme de packages .rpm (Rocky Linux 8, 9 et AlmaLinux 10),
construits intégralement dans Docker avec fpm.
formats:
- rpm
target_distros:
rpm:
- rocky-linux-8
- rocky-linux-9
- almalinux-10
- rhel-8+
- rhel-9+
- rhel-10+
tool: fpm
Build, tests et packaging RPM sont exécutés intégralement dans des conteneurs
via un multistage build.
build_pipeline:
dockerfile: Dockerfile.package
multi_stage: true
stages:
- name: builder
- name: test_and_compile
base: golang:latest
description: >
Compilation du binaire Go avec CGO_ENABLED=0 pour un binaire statique.
GOOS=linux GOARCH=amd64.
- name: rpm_rocky8_builder
go test ./... (échec si couverture < 80 %), puis compilation dun binaire
statique (CGO_ENABLED=0, GOOS=linux, GOARCH=amd64).
- name: rpm_builder
base: ruby:alpine
description: >
Construction du package RPM pour Rocky Linux 8 (el8) avec fpm.
- name: rpm_rocky9_builder
Installation de fpm, git et outils RPM. Génération du changelog RPM à
partir de lhistorique. Construction des .rpm pour les différentes
distributions.
- name: output_export
base: scratch
description: >
Construction du package RPM pour Rocky Linux 9 (el9) avec fpm.
- name: rpm_almalinux10_builder
description: >
Construction du package RPM pour AlmaLinux 10 (el10) avec fpm.
- name: output
description: >
Image Alpine minimale contenant les packages dans
/packages/rpm/{rocky8,rocky9,almalinux10}.
files:
binary:
source: dist/logcorrelator
dest: /usr/bin/logcorrelator
mode: "0755"
config:
- source: config.example.yml
dest: /etc/logcorrelator/logcorrelator.yml
mode: "0640"
config_file: true
- source: config.example.yml
dest: /usr/share/logcorrelator/logcorrelator.yml.example
mode: "0640"
directories:
- path: /var/log/logcorrelator
mode: "0755"
- path: /var/run/logcorrelator
mode: "0755"
- path: /etc/logcorrelator
mode: "0750"
maintainer_scripts:
rpm:
post: packaging/rpm/post
preun: packaging/rpm/preun
postun: packaging/rpm/postun
dependencies:
rpm:
- systemd
verify:
rpm:
rocky8:
command: docker run --rm -v $(pwd)/dist/rpm/rocky8:/packages rockylinux:8 sh -c "dnf install -y /packages/*.rpm"
rocky9:
command: docker run --rm -v $(pwd)/dist/rpm/rocky9:/packages rockylinux:9 sh -c "dnf install -y /packages/*.rpm"
almalinux10:
command: docker run --rm -v $(pwd)/dist/rpm/almalinux10:/packages almalinux:10 sh -c "dnf install -y /packages/*.rpm"
non_functional:
performance:
target_latency_ms: 1000
description: >
Latence visée < 1 s entre réception et insertion ClickHouse, avec
batching léger.
reliability:
drop_on_clickhouse_failure: true
description: >
En cas de ClickHouse lent/HS, les logs sont drop audelà du buffer pour
protéger la machine.
security:
user_separation: true
privileges: least
description: >
Service sous utilisateur dédié, pas de secrets en clair dans les logs,
principe de moindre privilège.
Étape minimale pour exposer les paquets RPM produits (docker build --output).