feat(backend): consolidation finale Spec 001 LCB-FT + Flyway V1-V5

Migrations Flyway (consolidées) :
- V1 : Schéma complet (69 tables, 1322 lignes)
- V2 : Colonnes BaseEntity (cree_par, modifie_par)
- V3 : Colonnes métier manquantes (adresses, alert_configuration)
- V4 : Correction system_logs (renommage colonnes, ajout timestamp)
- V5 : Nettoyage alert_configuration (suppression colonnes obsolètes)
- Suppression V2-V6 obsolètes (fragmentés)

Entités LCB-FT :
- AlerteLcbFt : Alertes anti-blanchiment
- AlertConfiguration : Configuration alertes
- SystemAlert : Alertes système
- SystemLog : Logs techniques (DÉJÀ COMMITÉE avec super.onCreate fix)

Services LCB-FT (T015, T016) :
- AlerteLcbFtService + Resource : Dashboard alertes admin
- AlertMonitoringService : Surveillance transactions
- SystemLoggingService : Logs centralisés
- FileStorageService : Upload documents

Repositories :
- AlerteLcbFtRepository
- AlertConfigurationRepository
- SystemAlertRepository
- SystemLogRepository

Tests :
- GlobalExceptionMapperTest : 17 erreurs corrigées (toResponse())

Spec 001 : 27/27 tâches (100%)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-03-16 05:15:17 +00:00
parent d8e3f23ec4
commit 347d89cc02
22 changed files with 3668 additions and 3129 deletions

View File

@@ -0,0 +1,249 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.AlertConfiguration;
import dev.lions.unionflow.server.entity.SystemAlert;
import dev.lions.unionflow.server.repository.AlertConfigurationRepository;
import dev.lions.unionflow.server.repository.SystemAlertRepository;
import dev.lions.unionflow.server.repository.SystemLogRepository;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.time.LocalDateTime;
/**
* Service de monitoring automatique qui vérifie les métriques système
* et génère des alertes automatiquement selon la configuration.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-15
*/
@Slf4j
@ApplicationScoped
public class AlertMonitoringService {
@Inject
AlertConfigurationRepository alertConfigurationRepository;
@Inject
SystemAlertRepository systemAlertRepository;
@Inject
SystemLogRepository systemLogRepository;
// Stocker la dernière valeur CPU pour détecter les dépassements prolongés
private Double lastCpuUsage = 0.0;
private LocalDateTime lastCpuHighTime = null;
/**
* Scheduler qui s'exécute toutes les minutes pour vérifier les métriques
*/
@Scheduled(cron = "0 * * * * ?") // Toutes les minutes à la seconde 0
@Transactional
public void monitorSystemMetrics() {
try {
log.debug("Running scheduled system metrics monitoring...");
AlertConfiguration config = alertConfigurationRepository.getConfiguration();
// Vérifier CPU
if (config.getCpuHighAlertEnabled()) {
checkCpuThreshold(config);
}
// Vérifier mémoire
if (config.getMemoryLowAlertEnabled()) {
checkMemoryThreshold(config);
}
// Vérifier erreurs critiques
if (config.getCriticalErrorAlertEnabled()) {
checkCriticalErrors();
}
} catch (Exception e) {
log.error("Error in scheduled system monitoring", e);
}
}
/**
* Vérifier si le CPU dépasse le seuil configuré
*/
private void checkCpuThreshold(AlertConfiguration config) {
try {
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
double loadAvg = osBean.getSystemLoadAverage();
int processors = osBean.getAvailableProcessors();
// Calculer l'utilisation CPU en pourcentage
double cpuUsage = loadAvg < 0 ? 0.0 : Math.min(100.0, (loadAvg / processors) * 100.0);
lastCpuUsage = cpuUsage;
int threshold = config.getCpuThresholdPercent();
int durationMinutes = config.getCpuDurationMinutes();
// Vérifier si le seuil est dépassé
if (cpuUsage > threshold) {
if (lastCpuHighTime == null) {
lastCpuHighTime = LocalDateTime.now();
} else {
// Vérifier si le dépassement dure depuis assez longtemps
LocalDateTime now = LocalDateTime.now();
long minutesSinceHigh = java.time.Duration.between(lastCpuHighTime, now).toMinutes();
if (minutesSinceHigh >= durationMinutes) {
// Créer une alerte seulement si pas déjà créée récemment
if (!hasRecentCpuAlert()) {
createCpuAlert(cpuUsage, threshold);
log.warn("CPU alert created: {}% (threshold: {}%)", cpuUsage, threshold);
}
}
}
} else {
// Reset le compteur si CPU revient sous le seuil
lastCpuHighTime = null;
}
} catch (Exception e) {
log.error("Error checking CPU threshold", e);
}
}
/**
* Vérifier si la mémoire dépasse le seuil configuré
*/
private void checkMemoryThreshold(AlertConfiguration config) {
try {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
long maxMemory = memoryBean.getHeapMemoryUsage().getMax();
long usedMemory = memoryBean.getHeapMemoryUsage().getUsed();
double memoryUsage = maxMemory > 0 ? (usedMemory * 100.0 / maxMemory) : 0.0;
int threshold = config.getMemoryThresholdPercent();
if (memoryUsage > threshold) {
// Créer une alerte seulement si pas déjà créée récemment
if (!hasRecentMemoryAlert()) {
createMemoryAlert(memoryUsage, threshold);
log.warn("Memory alert created: {}% (threshold: {}%)", memoryUsage, threshold);
}
}
} catch (Exception e) {
log.error("Error checking memory threshold", e);
}
}
/**
* Vérifier le nombre d'erreurs critiques dans la dernière heure
*/
private void checkCriticalErrors() {
try {
long criticalCount = systemLogRepository.countByLevelLast24h("CRITICAL");
// Si plus de 5 erreurs critiques dans les dernières 24h, créer une alerte
if (criticalCount > 5) {
if (!hasRecentCriticalErrorsAlert()) {
createCriticalErrorsAlert(criticalCount);
log.warn("Critical errors alert created: {} errors in last 24h", criticalCount);
}
}
} catch (Exception e) {
log.error("Error checking critical errors", e);
}
}
/**
* Créer une alerte CPU
*/
private void createCpuAlert(double currentValue, int threshold) {
SystemAlert alert = new SystemAlert();
alert.setLevel("WARNING");
alert.setTitle("Utilisation CPU élevée");
alert.setMessage(String.format("L'utilisation CPU dépasse le seuil configuré"));
alert.setTimestamp(LocalDateTime.now());
alert.setAcknowledged(false);
alert.setSource("CPU");
alert.setAlertType("THRESHOLD");
alert.setCurrentValue(currentValue);
alert.setThresholdValue((double) threshold);
alert.setUnit("%");
alert.setRecommendedActions("Vérifier les processus en cours d'exécution. Redémarrer les services si nécessaire.");
systemAlertRepository.persist(alert);
}
/**
* Créer une alerte mémoire
*/
private void createMemoryAlert(double currentValue, int threshold) {
SystemAlert alert = new SystemAlert();
alert.setLevel("WARNING");
alert.setTitle("Utilisation mémoire élevée");
alert.setMessage(String.format("L'utilisation mémoire dépasse le seuil configuré"));
alert.setTimestamp(LocalDateTime.now());
alert.setAcknowledged(false);
alert.setSource("MEMORY");
alert.setAlertType("THRESHOLD");
alert.setCurrentValue(currentValue);
alert.setThresholdValue((double) threshold);
alert.setUnit("%");
alert.setRecommendedActions("Vérifier les applications consommant de la mémoire. Considérer l'augmentation des ressources.");
systemAlertRepository.persist(alert);
}
/**
* Créer une alerte pour erreurs critiques
*/
private void createCriticalErrorsAlert(long errorCount) {
SystemAlert alert = new SystemAlert();
alert.setLevel("CRITICAL");
alert.setTitle("Erreurs critiques détectées");
alert.setMessage(String.format("%d erreurs critiques détectées dans les dernières 24h", errorCount));
alert.setTimestamp(LocalDateTime.now());
alert.setAcknowledged(false);
alert.setSource("System");
alert.setAlertType("ERROR");
alert.setCurrentValue((double) errorCount);
alert.setThresholdValue(5.0);
alert.setUnit("erreurs");
alert.setRecommendedActions("Consulter les logs système pour identifier la cause. Corriger les erreurs critiques.");
systemAlertRepository.persist(alert);
}
/**
* Vérifier si une alerte CPU a été créée récemment (dans les 30 dernières minutes)
*/
private boolean hasRecentCpuAlert() {
LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30);
return systemAlertRepository.findByTimestampBetween(thirtyMinutesAgo, LocalDateTime.now()).stream()
.anyMatch(alert -> "CPU".equals(alert.getSource()) && !alert.getAcknowledged());
}
/**
* Vérifier si une alerte mémoire a été créée récemment (dans les 30 dernières minutes)
*/
private boolean hasRecentMemoryAlert() {
LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30);
return systemAlertRepository.findByTimestampBetween(thirtyMinutesAgo, LocalDateTime.now()).stream()
.anyMatch(alert -> "MEMORY".equals(alert.getSource()) && !alert.getAcknowledged());
}
/**
* Vérifier si une alerte erreurs critiques a été créée récemment (dans la dernière heure)
*/
private boolean hasRecentCriticalErrorsAlert() {
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
return systemAlertRepository.findByTimestampBetween(oneHourAgo, LocalDateTime.now()).stream()
.anyMatch(alert -> "System".equals(alert.getSource()) && "ERROR".equals(alert.getAlertType()) && !alert.getAcknowledged());
}
}

View File

@@ -0,0 +1,162 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.AlerteLcbFt;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.AlerteLcbFtRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Service pour la génération automatique d'alertes LCB-FT.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-15
*/
@Slf4j
@ApplicationScoped
public class AlerteLcbFtService {
@Inject
AlerteLcbFtRepository alerteLcbFtRepository;
@Inject
OrganisationRepository organisationRepository;
@Inject
MembreRepository membreRepository;
/**
* Génère une alerte lorsqu'un seuil LCB-FT est dépassé.
*/
@Transactional
public void genererAlerteSeuilDepasse(
UUID organisationId,
UUID membreId,
String typeOperation,
BigDecimal montant,
BigDecimal seuil,
String transactionRef,
String origineFonds
) {
try {
Organisation organisation = organisationRepository.findByIdOptional(organisationId)
.orElse(null);
Membre membre = membreId != null ? membreRepository.findByIdOptional(membreId).orElse(null) : null;
String description = String.format(
"Transaction %s de %s FCFA dépassant le seuil LCB-FT de %s FCFA",
typeOperation != null ? typeOperation : "inconnue",
montant != null ? montant.toPlainString() : "0",
seuil != null ? seuil.toPlainString() : "0"
);
String details = String.format(
"Référence: %s | Origine des fonds: %s | Écart: %s FCFA",
transactionRef != null ? transactionRef : "N/A",
origineFonds != null && !origineFonds.isBlank() ? origineFonds : "NON FOURNI",
montant != null && seuil != null ? montant.subtract(seuil).toPlainString() : "N/A"
);
// Déterminer la sévérité selon l'écart avec le seuil
String severite = "INFO";
if (montant != null && seuil != null) {
BigDecimal ecart = montant.subtract(seuil);
BigDecimal ratio = seuil.compareTo(BigDecimal.ZERO) > 0 ?
ecart.divide(seuil, 2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
if (ratio.compareTo(new BigDecimal("2.0")) >= 0) { // > 200% du seuil
severite = "CRITICAL";
} else if (ratio.compareTo(new BigDecimal("0.5")) >= 0) { // > 50% du seuil
severite = "WARNING";
}
}
// Vérifier si origine des fonds est fournie
if (origineFonds == null || origineFonds.isBlank()) {
details += " | ⚠️ JUSTIFICATION MANQUANTE";
severite = "WARNING"; // Toujours WARNING minimum si pas de justification
}
AlerteLcbFt alerte = AlerteLcbFt.builder()
.organisation(organisation)
.membre(membre)
.typeAlerte("SEUIL_DEPASSE")
.dateAlerte(LocalDateTime.now())
.description(description)
.details(details)
.montant(montant)
.seuil(seuil)
.typeOperation(typeOperation)
.transactionRef(transactionRef)
.severite(severite)
.traitee(false)
.build();
alerteLcbFtRepository.persist(alerte);
log.info("Alerte LCB-FT générée : {} - Sévérité: {} - Montant: {} FCFA",
alerte.getId(), severite, montant);
} catch (Exception e) {
// Ne pas bloquer la transaction si la génération d'alerte échoue
log.error("Erreur lors de la génération d'alerte LCB-FT", e);
}
}
/**
* Génère une alerte pour justification manquante.
*/
@Transactional
public void genererAlerteJustificationManquante(
UUID organisationId,
UUID membreId,
String typeOperation,
BigDecimal montant,
String transactionRef
) {
try {
Organisation organisation = organisationRepository.findByIdOptional(organisationId)
.orElse(null);
Membre membre = membreId != null ? membreRepository.findByIdOptional(membreId).orElse(null) : null;
String description = String.format(
"Origine des fonds non fournie pour %s de %s FCFA",
typeOperation != null ? typeOperation : "transaction",
montant != null ? montant.toPlainString() : "0"
);
AlerteLcbFt alerte = AlerteLcbFt.builder()
.organisation(organisation)
.membre(membre)
.typeAlerte("JUSTIFICATION_MANQUANTE")
.dateAlerte(LocalDateTime.now())
.description(description)
.details("Référence: " + (transactionRef != null ? transactionRef : "N/A"))
.montant(montant)
.typeOperation(typeOperation)
.transactionRef(transactionRef)
.severite("WARNING")
.traitee(false)
.build();
alerteLcbFtRepository.persist(alerte);
log.warn("Alerte justification manquante générée pour transaction: {}", transactionRef);
} catch (Exception e) {
log.error("Erreur lors de la génération d'alerte justification manquante", e);
}
}
}

View File

@@ -0,0 +1,195 @@
package dev.lions.unionflow.server.service;
import jakarta.enterprise.context.ApplicationScoped;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* Service de stockage de fichiers sur le système de fichiers.
* Gère l'upload, le stockage, et le calcul des hash.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-15
*/
@Slf4j
@ApplicationScoped
public class FileStorageService {
// Taille max: 5 MB
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024;
// Types MIME autorisés
private static final String[] ALLOWED_MIME_TYPES = {
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"application/pdf"
};
@ConfigProperty(name = "unionflow.upload.directory", defaultValue = "./uploads")
String uploadDirectory;
/**
* Stocke un fichier uploadé et retourne les métadonnées.
*
* @param inputStream Flux du fichier
* @param fileName Nom du fichier original
* @param mimeType Type MIME
* @param fileSize Taille du fichier
* @return Métadonnées du fichier stocké
* @throws IOException Si erreur d'I/O
* @throws IllegalArgumentException Si validation échoue
*/
public FileMetadata storeFile(InputStream inputStream, String fileName, String mimeType, long fileSize)
throws IOException {
// Validation de la taille
if (fileSize > MAX_FILE_SIZE) {
throw new IllegalArgumentException(
String.format("Fichier trop volumineux. Taille max: %d MB", MAX_FILE_SIZE / (1024 * 1024))
);
}
// Validation du type MIME
if (!isAllowedMimeType(mimeType)) {
throw new IllegalArgumentException(
"Type de fichier non autorisé. Types acceptés: JPEG, PNG, GIF, PDF"
);
}
// Générer un nom unique pour le fichier
String uniqueFileName = generateUniqueFileName(fileName);
// Créer le chemin de stockage (organisé par date: uploads/2026/03/15/)
String relativePath = getRelativePath(uniqueFileName);
Path targetPath = Paths.get(uploadDirectory, relativePath);
// Créer les répertoires si nécessaire
Files.createDirectories(targetPath.getParent());
// Écrire le fichier et calculer les hash simultanément
MessageDigest md5 = null;
MessageDigest sha256 = null;
try {
md5 = MessageDigest.getInstance("MD5");
sha256 = MessageDigest.getInstance("SHA-256");
} catch (Exception e) {
log.warn("Impossible de créer les MessageDigest pour hash: {}", e.getMessage());
}
byte[] buffer = new byte[8192];
int bytesRead;
long totalBytes = 0;
try (FileOutputStream fos = new FileOutputStream(targetPath.toFile())) {
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
// Calculer les hash
if (md5 != null) {
md5.update(buffer, 0, bytesRead);
}
if (sha256 != null) {
sha256.update(buffer, 0, bytesRead);
}
}
}
// Générer les hash
String hashMd5 = md5 != null ? bytesToHex(md5.digest()) : null;
String hashSha256 = sha256 != null ? bytesToHex(sha256.digest()) : null;
log.info("Fichier stocké : {} ({} octets) - MD5: {}", uniqueFileName, totalBytes, hashMd5);
return FileMetadata.builder()
.nomFichier(uniqueFileName)
.nomOriginal(fileName)
.cheminStockage(relativePath)
.typeMime(mimeType)
.tailleOctets(totalBytes)
.hashMd5(hashMd5)
.hashSha256(hashSha256)
.build();
}
/**
* Vérifie si le type MIME est autorisé
*/
private boolean isAllowedMimeType(String mimeType) {
if (mimeType == null) {
return false;
}
for (String allowed : ALLOWED_MIME_TYPES) {
if (allowed.equalsIgnoreCase(mimeType)) {
return true;
}
}
return false;
}
/**
* Génère un nom unique pour le fichier
*/
private String generateUniqueFileName(String originalFileName) {
String extension = "";
int lastDot = originalFileName.lastIndexOf('.');
if (lastDot > 0) {
extension = originalFileName.substring(lastDot);
}
return UUID.randomUUID().toString() + extension;
}
/**
* Retourne le chemin relatif organisé par date (YYYY/MM/DD/)
*/
private String getRelativePath(String fileName) {
LocalDate today = LocalDate.now();
return String.format("%04d/%02d/%02d/%s",
today.getYear(),
today.getMonthValue(),
today.getDayOfMonth(),
fileName
);
}
/**
* Convertit un tableau de bytes en hexadécimal
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* Classe pour encapsuler les métadonnées d'un fichier stocké
*/
@lombok.Data
@lombok.Builder
public static class FileMetadata {
private String nomFichier;
private String nomOriginal;
private String cheminStockage;
private String typeMime;
private Long tailleOctets;
private String hashMd5;
private String hashSha256;
}
}

View File

@@ -6,7 +6,15 @@ import dev.lions.unionflow.server.api.dto.logs.response.AlertConfigResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemAlertResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemLogResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse;
import dev.lions.unionflow.server.entity.AlertConfiguration;
import dev.lions.unionflow.server.entity.SystemAlert;
import dev.lions.unionflow.server.entity.SystemLog;
import dev.lions.unionflow.server.repository.AlertConfigurationRepository;
import dev.lions.unionflow.server.repository.SystemAlertRepository;
import dev.lions.unionflow.server.repository.SystemLogRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.lang.management.ManagementFactory;
@@ -27,6 +35,15 @@ public class LogsMonitoringService {
private final LocalDateTime systemStartTime = LocalDateTime.now();
@Inject
SystemLogRepository systemLogRepository;
@Inject
SystemAlertRepository systemAlertRepository;
@Inject
AlertConfigurationRepository alertConfigurationRepository;
/**
* Rechercher dans les logs système
*/
@@ -34,34 +51,41 @@ public class LogsMonitoringService {
log.debug("Recherche de logs avec filtres: level={}, source={}, query={}",
request.getLevel(), request.getSource(), request.getSearchQuery());
// Dans une vraie implémentation, on interrogerait une DB ou un système de logs
// Pour l'instant, on retourne des données de test
List<SystemLogResponse> allLogs = generateMockLogs();
// Calculer les dates de début et fin selon timeRange
LocalDateTime start = null;
LocalDateTime end = LocalDateTime.now();
// Filtrage par niveau
if (request.getLevel() != null && !"TOUS".equals(request.getLevel())) {
allLogs = allLogs.stream()
.filter(log -> log.getLevel().equals(request.getLevel()))
.collect(Collectors.toList());
if (request.getTimeRange() != null) {
switch (request.getTimeRange()) {
case "1H" -> start = end.minusHours(1);
case "24H" -> start = end.minusHours(24);
case "7D" -> start = end.minusDays(7);
case "30D" -> start = end.minusDays(30);
}
}
// Filtrage par source
if (request.getSource() != null && !"TOUS".equals(request.getSource())) {
allLogs = allLogs.stream()
.filter(log -> log.getSource().equals(request.getSource()))
.collect(Collectors.toList());
}
// Rechercher dans la BDD
int offset = request.getOffset() != null ? request.getOffset() : 0;
int limit = request.getLimit() != null ? request.getLimit() : 100;
// Filtrage par recherche textuelle
if (request.getSearchQuery() != null && !request.getSearchQuery().isBlank()) {
String query = request.getSearchQuery().toLowerCase();
allLogs = allLogs.stream()
.filter(log -> log.getMessage().toLowerCase().contains(query)
|| log.getSource().toLowerCase().contains(query))
.collect(Collectors.toList());
}
// Convertir offset/limit en page/pageSize
int pageSize = limit;
int pageIndex = offset / pageSize;
return allLogs;
List<SystemLog> logs = systemLogRepository.search(
request.getLevel(),
request.getSource(),
request.getSearchQuery(),
start,
end,
pageIndex,
pageSize
);
// Mapper vers DTO
return logs.stream()
.map(this::mapToLogResponse)
.collect(Collectors.toList());
}
/**
@@ -127,7 +151,7 @@ public class LogsMonitoringService {
.networkUsageMbps(12.3 + ThreadLocalRandom.current().nextDouble(5))
.activeConnections(1200 + ThreadLocalRandom.current().nextInt(100))
.errorRate(0.02)
.averageResponseTimeMs(127L)
.averageResponseTimeMs(127.0)
.uptime(uptimeMs)
.uptimeFormatted(uptimeFormatted)
.services(services)
@@ -145,48 +169,26 @@ public class LogsMonitoringService {
public List<SystemAlertResponse> getActiveAlerts() {
log.debug("Récupération des alertes actives");
// Dans une vraie implémentation, on interrogerait la DB
List<SystemAlertResponse> alerts = new ArrayList<>();
List<SystemAlert> alerts = systemAlertRepository.findActiveAlerts();
alerts.add(SystemAlertResponse.builder()
.id(UUID.randomUUID())
.level("WARNING")
.title("CPU élevé")
.message("Utilisation CPU > 80% pendant 5 minutes")
.timestamp(LocalDateTime.now().minusMinutes(12))
.acknowledged(false)
.source("CPU")
.alertType("THRESHOLD")
.currentValue(85.5)
.thresholdValue(80.0)
.build());
alerts.add(SystemAlertResponse.builder()
.id(UUID.randomUUID())
.level("INFO")
.title("Sauvegarde terminée")
.message("Sauvegarde automatique réussie (2.3 GB)")
.timestamp(LocalDateTime.now().minusHours(2))
.acknowledged(true)
.acknowledgedBy("admin@unionflow.test")
.acknowledgedAt(LocalDateTime.now().minusHours(1).minusMinutes(50))
.source("BACKUP")
.alertType("INFO")
.build());
return alerts;
return alerts.stream()
.map(this::mapToAlertResponse)
.collect(Collectors.toList());
}
/**
* Acquitter une alerte
*/
@Transactional
public void acknowledgeAlert(UUID alertId) {
log.info("Acquittement de l'alerte: {}", alertId);
// Dans une vraie implémentation, on mettrait à jour en DB
// TODO: Marquer l'alerte comme acquittée en DB
// TODO: Récupérer l'utilisateur courant depuis le contexte de sécurité
String currentUser = "admin@unionflow.test"; // Temporaire
log.info("Alerte acquittée avec succès");
systemAlertRepository.acknowledgeAlert(alertId, currentUser);
log.info("Alerte {} acquittée avec succès par {}", alertId, currentUser);
}
/**
@@ -195,137 +197,131 @@ public class LogsMonitoringService {
public AlertConfigResponse getAlertConfig() {
log.debug("Récupération de la configuration des alertes");
AlertConfiguration config = alertConfigurationRepository.getConfiguration();
// Compter les alertes réelles
long totalLast24h = systemAlertRepository.countLast24h();
long activeAlerts = systemAlertRepository.countActive();
long acknowledgedLast24h = systemAlertRepository.countAcknowledgedLast24h();
return AlertConfigResponse.builder()
.cpuHighAlertEnabled(true)
.cpuThresholdPercent(80)
.cpuDurationMinutes(5)
.memoryLowAlertEnabled(true)
.memoryThresholdPercent(85)
.criticalErrorAlertEnabled(true)
.errorAlertEnabled(true)
.connectionFailureAlertEnabled(true)
.connectionFailureThreshold(100)
.connectionFailureWindowMinutes(5)
.emailNotificationsEnabled(true)
.pushNotificationsEnabled(false)
.smsNotificationsEnabled(false)
.alertEmailRecipients("admin@unionflow.test,support@unionflow.test")
.totalAlertsLast24h(15)
.activeAlerts(2)
.acknowledgedAlerts(13)
.cpuHighAlertEnabled(config.getCpuHighAlertEnabled())
.cpuThresholdPercent(config.getCpuThresholdPercent())
.cpuDurationMinutes(config.getCpuDurationMinutes())
.memoryLowAlertEnabled(config.getMemoryLowAlertEnabled())
.memoryThresholdPercent(config.getMemoryThresholdPercent())
.criticalErrorAlertEnabled(config.getCriticalErrorAlertEnabled())
.errorAlertEnabled(config.getErrorAlertEnabled())
.connectionFailureAlertEnabled(config.getConnectionFailureAlertEnabled())
.connectionFailureThreshold(config.getConnectionFailureThreshold())
.connectionFailureWindowMinutes(config.getConnectionFailureWindowMinutes())
.emailNotificationsEnabled(config.getEmailNotificationsEnabled())
.pushNotificationsEnabled(config.getPushNotificationsEnabled())
.smsNotificationsEnabled(config.getSmsNotificationsEnabled())
.alertEmailRecipients(config.getAlertEmailRecipients())
.totalAlertsLast24h((int) totalLast24h)
.activeAlerts((int) activeAlerts)
.acknowledgedAlerts((int) acknowledgedLast24h)
.build();
}
/**
* Mettre à jour la configuration des alertes
*/
@Transactional
public AlertConfigResponse updateAlertConfig(UpdateAlertConfigRequest request) {
log.info("Mise à jour de la configuration des alertes");
// Dans une vraie implémentation, on persisterait en DB
// Pour l'instant, on retourne juste la config avec les nouvelles valeurs
// Mapper request vers entité
AlertConfiguration config = new AlertConfiguration();
config.setCpuHighAlertEnabled(request.getCpuHighAlertEnabled());
config.setCpuThresholdPercent(request.getCpuThresholdPercent());
config.setCpuDurationMinutes(request.getCpuDurationMinutes());
config.setMemoryLowAlertEnabled(request.getMemoryLowAlertEnabled());
config.setMemoryThresholdPercent(request.getMemoryThresholdPercent());
config.setCriticalErrorAlertEnabled(request.getCriticalErrorAlertEnabled());
config.setErrorAlertEnabled(request.getErrorAlertEnabled());
config.setConnectionFailureAlertEnabled(request.getConnectionFailureAlertEnabled());
config.setConnectionFailureThreshold(request.getConnectionFailureThreshold());
config.setConnectionFailureWindowMinutes(request.getConnectionFailureWindowMinutes());
config.setEmailNotificationsEnabled(request.getEmailNotificationsEnabled());
config.setPushNotificationsEnabled(request.getPushNotificationsEnabled());
config.setSmsNotificationsEnabled(request.getSmsNotificationsEnabled());
config.setAlertEmailRecipients(request.getAlertEmailRecipients());
// Persister
AlertConfiguration updated = alertConfigurationRepository.updateConfiguration(config);
// Compter les alertes
long totalLast24h = systemAlertRepository.countLast24h();
long activeAlerts = systemAlertRepository.countActive();
long acknowledgedLast24h = systemAlertRepository.countAcknowledgedLast24h();
// Mapper vers response
return AlertConfigResponse.builder()
.cpuHighAlertEnabled(request.getCpuHighAlertEnabled())
.cpuThresholdPercent(request.getCpuThresholdPercent())
.cpuDurationMinutes(request.getCpuDurationMinutes())
.memoryLowAlertEnabled(request.getMemoryLowAlertEnabled())
.memoryThresholdPercent(request.getMemoryThresholdPercent())
.criticalErrorAlertEnabled(request.getCriticalErrorAlertEnabled())
.errorAlertEnabled(request.getErrorAlertEnabled())
.connectionFailureAlertEnabled(request.getConnectionFailureAlertEnabled())
.connectionFailureThreshold(request.getConnectionFailureThreshold())
.connectionFailureWindowMinutes(request.getConnectionFailureWindowMinutes())
.emailNotificationsEnabled(request.getEmailNotificationsEnabled())
.pushNotificationsEnabled(request.getPushNotificationsEnabled())
.smsNotificationsEnabled(request.getSmsNotificationsEnabled())
.alertEmailRecipients(request.getAlertEmailRecipients())
.totalAlertsLast24h(15)
.activeAlerts(2)
.acknowledgedAlerts(13)
.cpuHighAlertEnabled(updated.getCpuHighAlertEnabled())
.cpuThresholdPercent(updated.getCpuThresholdPercent())
.cpuDurationMinutes(updated.getCpuDurationMinutes())
.memoryLowAlertEnabled(updated.getMemoryLowAlertEnabled())
.memoryThresholdPercent(updated.getMemoryThresholdPercent())
.criticalErrorAlertEnabled(updated.getCriticalErrorAlertEnabled())
.errorAlertEnabled(updated.getErrorAlertEnabled())
.connectionFailureAlertEnabled(updated.getConnectionFailureAlertEnabled())
.connectionFailureThreshold(updated.getConnectionFailureThreshold())
.connectionFailureWindowMinutes(updated.getConnectionFailureWindowMinutes())
.emailNotificationsEnabled(updated.getEmailNotificationsEnabled())
.pushNotificationsEnabled(updated.getPushNotificationsEnabled())
.smsNotificationsEnabled(updated.getSmsNotificationsEnabled())
.alertEmailRecipients(updated.getAlertEmailRecipients())
.totalAlertsLast24h((int) totalLast24h)
.activeAlerts((int) activeAlerts)
.acknowledgedAlerts((int) acknowledgedLast24h)
.build();
}
// ==================== MÉTHODES DE MAPPING ====================
/**
* Générer des logs de test (à remplacer par une vraie source de logs)
* Mapper SystemLog vers SystemLogResponse
*/
private List<SystemLogResponse> generateMockLogs() {
List<SystemLogResponse> logs = new ArrayList<>();
private SystemLogResponse mapToLogResponse(SystemLog log) {
SystemLogResponse response = SystemLogResponse.builder()
.level(log.getLevel())
.source(log.getSource())
.message(log.getMessage())
.details(log.getDetails())
.timestamp(log.getTimestamp())
.userId(log.getUserId())
.ipAddress(log.getIpAddress())
.sessionId(log.getSessionId())
.endpoint(log.getEndpoint())
.httpStatusCode(log.getHttpStatusCode())
.build();
response.setId(log.getId());
return response;
}
logs.add(SystemLogResponse.builder()
.id(UUID.randomUUID())
.level("CRITICAL")
.source("Database")
.message("Connexion à la base de données perdue")
.details("Pool de connexions épuisé")
.timestamp(LocalDateTime.now().minusMinutes(15))
.username("system")
.ipAddress("192.168.1.100")
.requestId(UUID.randomUUID().toString())
.build());
logs.add(SystemLogResponse.builder()
.id(UUID.randomUUID())
.level("ERROR")
.source("API")
.message("Erreur 500 sur /api/members")
.details("NullPointerException dans MemberService.findAll()")
.timestamp(LocalDateTime.now().minusMinutes(18))
.username("admin@test.com")
.ipAddress("192.168.1.101")
.requestId(UUID.randomUUID().toString())
.stackTrace("java.lang.NullPointerException\n\tat dev.lions.unionflow.server.service.MemberService.findAll(MemberService.java:45)")
.build());
logs.add(SystemLogResponse.builder()
.id(UUID.randomUUID())
.level("WARN")
.source("Auth")
.message("Tentative de connexion avec mot de passe incorrect")
.details("IP: 192.168.1.100 - Utilisateur: admin@test.com")
.timestamp(LocalDateTime.now().minusMinutes(20))
.username("admin@test.com")
.ipAddress("192.168.1.100")
.requestId(UUID.randomUUID().toString())
.build());
logs.add(SystemLogResponse.builder()
.id(UUID.randomUUID())
.level("INFO")
.source("System")
.message("Sauvegarde automatique terminée")
.details("Taille: 2.3 GB - Durée: 45 secondes")
.timestamp(LocalDateTime.now().minusHours(2))
.username("system")
.ipAddress("localhost")
.requestId(UUID.randomUUID().toString())
.build());
logs.add(SystemLogResponse.builder()
.id(UUID.randomUUID())
.level("DEBUG")
.source("Cache")
.message("Cache invalidé pour user_sessions")
.details("Raison: Expiration automatique")
.timestamp(LocalDateTime.now().minusMinutes(25))
.username("system")
.ipAddress("localhost")
.requestId(UUID.randomUUID().toString())
.build());
logs.add(SystemLogResponse.builder()
.id(UUID.randomUUID())
.level("TRACE")
.source("Performance")
.message("Requête SQL exécutée")
.details("SELECT * FROM members WHERE active = true - Durée: 23ms")
.timestamp(LocalDateTime.now().minusMinutes(27))
.username("system")
.ipAddress("localhost")
.requestId(UUID.randomUUID().toString())
.build());
return logs;
/**
* Mapper SystemAlert vers SystemAlertResponse
*/
private SystemAlertResponse mapToAlertResponse(SystemAlert alert) {
SystemAlertResponse response = SystemAlertResponse.builder()
.level(alert.getLevel())
.title(alert.getTitle())
.message(alert.getMessage())
.timestamp(alert.getTimestamp())
.acknowledged(alert.getAcknowledged())
.acknowledgedBy(alert.getAcknowledgedBy())
.acknowledgedAt(alert.getAcknowledgedAt())
.source(alert.getSource())
.alertType(alert.getAlertType())
.currentValue(alert.getCurrentValue())
.thresholdValue(alert.getThresholdValue())
.unit(alert.getUnit())
.recommendedActions(alert.getRecommendedActions())
.build();
response.setId(alert.getId());
return response;
}
/**

View File

@@ -0,0 +1,209 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.SystemLog;
import dev.lions.unionflow.server.repository.SystemLogRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
/**
* Service centralisé pour la création de logs système.
* Gère la persistence des logs dans la base de données.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-15
*/
@Slf4j
@ApplicationScoped
public class SystemLoggingService {
@Inject
SystemLogRepository systemLogRepository;
/**
* Logger une requête HTTP
*/
@Transactional
public void logRequest(
String method,
String endpoint,
Integer httpStatusCode,
String userId,
String ipAddress,
String sessionId,
Long durationMs
) {
try {
SystemLog systemLog = new SystemLog();
systemLog.setLevel(getLogLevelFromStatusCode(httpStatusCode));
systemLog.setSource("API");
systemLog.setMessage(String.format("%s %s - %d (%dms)", method, endpoint, httpStatusCode, durationMs));
systemLog.setDetails(String.format("User: %s, IP: %s, Duration: %dms", userId, ipAddress, durationMs));
systemLog.setTimestamp(LocalDateTime.now());
systemLog.setUserId(userId);
systemLog.setIpAddress(ipAddress);
systemLog.setSessionId(sessionId);
systemLog.setEndpoint(endpoint);
systemLog.setHttpStatusCode(httpStatusCode);
systemLogRepository.persist(systemLog);
} catch (Exception e) {
// Ne pas propager les erreurs de logging pour ne pas casser l'application
log.error("Failed to persist request log", e);
}
}
/**
* Logger une erreur (exception)
*/
@Transactional
public void logError(
String source,
String message,
String details,
String userId,
String ipAddress,
String endpoint,
Integer httpStatusCode
) {
try {
SystemLog systemLog = new SystemLog();
systemLog.setLevel("ERROR");
systemLog.setSource(source);
systemLog.setMessage(message);
systemLog.setDetails(details);
systemLog.setTimestamp(LocalDateTime.now());
systemLog.setUserId(userId);
systemLog.setIpAddress(ipAddress);
systemLog.setEndpoint(endpoint);
systemLog.setHttpStatusCode(httpStatusCode);
systemLogRepository.persist(systemLog);
} catch (Exception e) {
log.error("Failed to persist error log", e);
}
}
/**
* Logger une erreur critique
*/
@Transactional
public void logCritical(
String source,
String message,
String details,
String userId,
String ipAddress
) {
try {
SystemLog systemLog = new SystemLog();
systemLog.setLevel("CRITICAL");
systemLog.setSource(source);
systemLog.setMessage(message);
systemLog.setDetails(details);
systemLog.setTimestamp(LocalDateTime.now());
systemLog.setUserId(userId);
systemLog.setIpAddress(ipAddress);
systemLogRepository.persist(systemLog);
} catch (Exception e) {
log.error("Failed to persist critical log", e);
}
}
/**
* Logger un warning
*/
@Transactional
public void logWarning(
String source,
String message,
String details,
String userId,
String ipAddress
) {
try {
SystemLog systemLog = new SystemLog();
systemLog.setLevel("WARNING");
systemLog.setSource(source);
systemLog.setMessage(message);
systemLog.setDetails(details);
systemLog.setTimestamp(LocalDateTime.now());
systemLog.setUserId(userId);
systemLog.setIpAddress(ipAddress);
systemLogRepository.persist(systemLog);
} catch (Exception e) {
log.error("Failed to persist warning log", e);
}
}
/**
* Logger une info
*/
@Transactional
public void logInfo(
String source,
String message,
String details
) {
try {
SystemLog systemLog = new SystemLog();
systemLog.setLevel("INFO");
systemLog.setSource(source);
systemLog.setMessage(message);
systemLog.setDetails(details);
systemLog.setTimestamp(LocalDateTime.now());
systemLogRepository.persist(systemLog);
} catch (Exception e) {
log.error("Failed to persist info log", e);
}
}
/**
* Logger un événement de debug
*/
@Transactional
public void logDebug(
String source,
String message,
String details
) {
try {
SystemLog systemLog = new SystemLog();
systemLog.setLevel("DEBUG");
systemLog.setSource(source);
systemLog.setMessage(message);
systemLog.setDetails(details);
systemLog.setTimestamp(LocalDateTime.now());
systemLogRepository.persist(systemLog);
} catch (Exception e) {
log.error("Failed to persist debug log", e);
}
}
/**
* Déterminer le niveau de log selon le code HTTP
*/
private String getLogLevelFromStatusCode(Integer statusCode) {
if (statusCode == null) {
return "INFO";
}
if (statusCode >= 500) {
return "ERROR";
} else if (statusCode >= 400) {
return "WARNING";
} else if (statusCode >= 300) {
return "INFO";
} else {
return "DEBUG";
}
}
}

View File

@@ -1,6 +1,6 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.system.response.SystemMetricsResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import io.agroal.api.AgroalDataSource;