All files / src/services uptimeChecker.js

0% Statements 0/57
0% Branches 0/30
0% Functions 0/6
0% Lines 0/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148                                                                                                                                                                                                                                                                                                       
/**
 * Service Uptime Checker — Ping les URLs surveillées à intervalles réguliers.
 *
 * Fonctionnalités :
 *   - Ping toutes les 60s, enregistre statut + latence
 *   - Détecte les transitions DOWN/UP et envoie des alertes (webhook + push)
 *   - Cooldown 5 min entre alertes identiques
 *   - Nettoie les données > 30 jours
 *
 * @module services/uptimeChecker
 */
 
const { pool: db } = require('../db');
const { notify } = require('./webhookNotifier');
const { getPlanLimits } = require('../config/plans');
 
const CHECK_INTERVAL_MS = 60 * 1000;
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 10000;
const ALERT_COOLDOWN_MS = 5 * 60 * 1000; // 5 min entre alertes identiques
 
// Stocke le dernier timestamp d'alerte par monitor (cooldown)
const lastAlertTime = new Map();
 
function isUp(status) {
  return status >= 200 && status < 400;
}
 
async function pingUrl(url) {
  const start = Date.now();
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
    const res = await fetch(url, {
      method: 'GET',
      signal: controller.signal,
      redirect: 'follow',
      headers: { 'User-Agent': 'IliaCloud-Uptime/1.0' },
    });
    clearTimeout(timeout);
    return { status: res.status, latency_ms: Date.now() - start };
  } catch {
    return { status: 0, latency_ms: Date.now() - start };
  }
}
 
async function checkAll() {
  try {
    const { rows: monitors } = await db.query(`
      SELECT m.id, m.url, m.label, m.interval_s, m.user_id,
             (SELECT created_at FROM uptime_history WHERE monitor_id = m.id ORDER BY created_at DESC LIMIT 1) AS last_check,
             (SELECT status FROM uptime_history WHERE monitor_id = m.id ORDER BY created_at DESC LIMIT 1) AS prev_status
      FROM uptime_monitors m
      WHERE m.enabled = TRUE
    `);
 
    const now = Date.now();
 
    for (const monitor of monitors) {
      const lastCheck = monitor.last_check ? new Date(monitor.last_check).getTime() : 0;
      if (now - lastCheck < monitor.interval_s * 1000) continue;
 
      const { status, latency_ms } = await pingUrl(monitor.url);
 
      await db.query(
        'INSERT INTO uptime_history (monitor_id, status, latency_ms) VALUES ($1, $2, $3)',
        [monitor.id, status, latency_ms],
      );
 
      // Détecter les transitions UP → DOWN et DOWN → UP
      const wasUp = monitor.prev_status == null || isUp(monitor.prev_status);
      const nowUp = isUp(status);
 
      if (wasUp && !nowUp) {
        // Site vient de tomber
        const lastAlert = lastAlertTime.get(monitor.id) || 0;
        if (now - lastAlert > ALERT_COOLDOWN_MS) {
          lastAlertTime.set(monitor.id, now);
          await notify(monitor.user_id, 'uptime_down', {
            title: `${monitor.label} est DOWN`,
            message: `${monitor.url} — HTTP ${status || 'timeout'} — Latence ${latency_ms}ms`,
            fields: [
              { name: 'URL', value: monitor.url, inline: true },
              { name: 'Statut', value: `${status || 'timeout'}`, inline: true },
            ],
          });
          console.log(`[uptime] ALERTE DOWN: ${monitor.label} (${monitor.url}) — ${status}`);
        }
      }
 
      if (!wasUp && nowUp) {
        // Site vient de remonter
        await notify(monitor.user_id, 'uptime_up', {
          title: `${monitor.label} est de retour UP`,
          message: `${monitor.url} — HTTP ${status} — Latence ${latency_ms}ms`,
          fields: [
            { name: 'URL', value: monitor.url, inline: true },
            { name: 'Latence', value: `${latency_ms}ms`, inline: true },
          ],
        });
        console.log(`[uptime] RETOUR UP: ${monitor.label} (${monitor.url}) — ${status}`);
      }
    }
  } catch (err) {
    console.error('[uptime] Erreur lors du check :', err.message);
  }
}
 
async function cleanup() {
  try {
    const { rows: users } = await db.query(
      `SELECT DISTINCT u.id, u.plan FROM users u
       JOIN uptime_monitors m ON m.user_id = u.id`,
    );
 
    let totalDeleted = 0;
 
    for (const user of users) {
      const limits = await getPlanLimits(user.plan || 'free');
      const retentionDays = limits.uptime_retention_days;
      if (retentionDays === Infinity) continue;
 
      const result = await db.query(
        `DELETE FROM uptime_history
         WHERE monitor_id IN (SELECT id FROM uptime_monitors WHERE user_id = $1)
         AND created_at < NOW() - INTERVAL '1 day' * $2`,
        [user.id, retentionDays],
      );
      totalDeleted += result.rowCount;
    }
 
    if (totalDeleted > 0) {
      console.log(`[uptime] Nettoyage par plan : ${totalDeleted} enregistrements supprimes`);
    }
  } catch (err) {
    console.error('[uptime] Erreur cleanup :', err.message);
  }
}
 
function startUptimeChecker() {
  console.log('[uptime] Démarrage du monitoring uptime (avec alertes)');
  setInterval(checkAll, CHECK_INTERVAL_MS);
  setInterval(cleanup, CLEANUP_INTERVAL_MS);
  setTimeout(checkAll, 10000);
}
 
module.exports = { startUptimeChecker };