Jednoduché monitorovanie dostupnosti webu a SMS notifikácia v prípade problémov

Publikoval Michal Kočí dňa 18.7.2015 o 11:00 v kategóriach Node.js a Raspberry Pi

Pokiaľ máte web alebo viacero webov, existujú nástroje, ktoré vám ich budú monitorovať. Ak sa vám však zdajú ako kanón na vrabce, alebo vám nevyhovujú či už kvôli cene, alebo poskytovanej funkcionalite, prečo si nenaprogramovať vlastné riešenie, napríklad v Node.js?

Moje veľmi jednoduché riešenie

Moje požiadavky boli veľmi jednoduché a od vašich sa môžu samozrejme líšiť. Výhodou vášho vlastného riešenie však je, že si ho viete rýchlo upraviť presne na mieru.

Takže, mám niekoľko webov a chcem v pravideľných intervaloch kontrolovať ich dostupnosť, t.j. či vracajú HTTP stavový kód 200 a nie nejakú chybu (napríklad chybový kód 500). Tiež chcem kontrolovať, ako rýchlo dostanem odpoveď a či netrvá príliš dlho.

V prípade problémov chcem byť o nich informovaný, najlepšie cez SMS, lebo tá dorazí ihneď aj bez pripojenia k internetu. Na posielanie SMS použijem platenú službu Twilio, lebo nie je drahá (1 dolár za jedno číslo mesačne a potom pár centov za jednu SMS), je spoľahlivá a má jednoduché API, cez ktoré sa dá ľahko ovládať. Vám môže vyhovovať iná, napríklad Clockwork či nejaká úplne iná.

V zásade mi bolo jedno, akú technológiu použiť, ale keďže mám Raspberry Pi s nainštalovaným Node.js a s rozbehaným Git deploymentom a pre Twilio existuje npm balíček, rozhodnovanie boľo ľahké - Node.js aplikácia spúšťaná v pravideľných intervaloch cez cron.

Chceli by ste sa rýchlo a ľahko naučiť programovať webové aplikácie v Javascripte a bežať ich na Node.js? Jednoduché. Rád vás to naučím na mojom školení Node.js - serverových aplikácií v Javascripte.

Nakoľko mi príde, že by sa to jednak mohlo niekomu hodiť a druhak to krásne ukazuje jedno z možstva možných použití Node.js, tak som sa rozhodol riešenie s vami zdieľať. Navyše je to o to lepšie, že ukazuje, že Node.js vám výborne poslúži aj na naprogramovanie aplikácie ktorá vôbec nemusí byť webom. Pozrieť si celý zdrojový kód môžete na Githube.

Nastavenia kontroly a Twilia

Aplikácia obsahuje nastavenia - aká URL, na akom webe má byť skontrolovaná a aká je maximálna doba na získanie odpovede. Konfigurácia jedného endpointu môže vyzerať nasledovne:

{
name: 'BING',
responseTimeout: 1000,
protocol: 'http',
request: {
hostname: 'www.bing.com',
port: 80,
path: '/', method: 'GET'
}
}

Nastavujem si názov endpointu (použijem pri notifikácii v prípade problémov), maximálny čas (ak je reálny čas väčší, považujem to za chybu), použitý protokol (http alebo https) a parametre requestu - doménu, url, port a metódu (GET, POST, ...).

Objekt request celý použijem pri volaní http.request, čiže si môžem konfigurovať ďalšie veci, ako HTTP hlavičky a podobne.

Nastavenia budem mať v module settings.js, kde okrem samotných endpointov pribudne aj nastavenie Twilia - čísla na ktoré sa má správa v prípade problémov odoslať a ostatné API nastavenia (SID účtu, autentifikačný token a číslo, z ktorého má byť SMS poslaná):

twilio: {
  mobiles: ['MY_PHONE_NUMBER', 'MY_OTHER_PHONE_NUMBER'],
  enabled: true,
  accountSid: 'MY_TWILIO_SID',
  authToken: 'MY_TWILIO_AUTH_TOKEN',
  number: 'MY_TWILIO_NUMBER'
}

Celé nastavenia (súbor resp. modul settings.js) potom vyzerajú takto:

module.exports = {
  twilio: {
    mobiles: ['MY_PHONE_NUMBER', 'MY_OTHER_PHONE_NUMBER'],
    enabled: true,
    accountSid: 'MY_TWILIO_SID',
    authToken: 'MY_TWILIO_AUTH_TOKEN',
    number: 'MY_TWILIO_NUMBER'
  },
  endpoints: [
    {
      name: 'BING',
      responseTimeout: 1000,
      protocol: 'http',
      request: {
        hostname: 'www.bing.com',
        port: 80,
        path: '/',
        method: 'GET'
      }
    }
  ]
}

Kontrola webov

Na samotnú kontrolu webov bude slúžiť modul endpoint.js, ten bude mať jedinú metódu a to execute (veľmi kreatívny názov, pravda). Tá prijíma jeden parameter - nastavenie práve jedného endpointu.

Metóda execute použije natívny http alebo https modul (v závislosti na protokole) a spraví request s našimi nastaveniami. Request je asynchrónny, preto metóda vráti promise (použijem modul q).

Na requeste a response budeme počúvať na niektoré udalosti, v ktorých budeme spracovávať dáta, merať čas či spracovávať chybové kódy. A samozrejme, budeme pri patričných udalostiach resolvovať samotný promise.

Začneme tým, že vytvoríme promise, pripravíme si objekt s výsledkom a do premennej protocol si priradíme http alebo https podľa použitého protokolu. A vypíšeme si do konzoly, že sme začali s kontrolou daného endpointu.

var deferred = q.defer();
var start;
var outcome = { options: options };
var protocol = options.protocol === 'https' ? https : http;

console.log('processing ' + options.name);

Ďalej si pripravíme request a priradíme funkcie na počúvanie udalostí (na odpovedi) data (slúži na vystavanie premennej s celou odpoveďou servera) a end (v tejto zmeriame koľko celý request trval, vypíšeme si to do konzoly a resolvneme promise):

var req = protocol.request(options.request, function(res) {
outcome.statusCode = res.statusCode;
outcome.body = '';
res.on('data', function(d) {
outcome.body += d;
});
res.on('end', function () {
outcome.elapsed = Math.round(process.hrtime(start)[1] / 1000000);
console.log('processed ' + options.name + ': ' + outcome.body.length + ' bytes in ' + outcome.elapsed + ' ms'); deferred.resolve(outcome);
});
});

A počúvať budeme aj na udalosti na requeste. Na udalosti socket (zaznačíme si čas začiatku) a error (na výpočet trvania, zistenie chybového kódu a resolvnutie promisu):

req.on('socket', function (res) {
  start = process.hrtime();
});
req.on('error', function(e) {
  outcome.elapsed = Math.round(process.hrtime(start)[1] / 1000000);
  outcome.errorCode = e.code;
  deferred.resolve(outcome);
});

V metóde potom spustíme request a vrátime samotný promise:

req.end();
return deferred.promise;

Celý modul potom vyzerá nasledovne:

var http = require('http');
var https = require('https');
var q = require('q');
var execute = function(options) {
  var deferred = q.defer();
  var start;
  var outcome = { options: options };
  var protocol = options.protocol === 'https' ? https : http;

  console.log('processing ' + options.name);

  var req = protocol.request(options.request, function(res) {
    outcome.statusCode = res.statusCode;
    outcome.body = '';
    res.on('data', function(d) {
      outcome.body += d;
    });
    res.on('end', function () {
      outcome.elapsed = Math.round(process.hrtime(start)[1] / 1000000);
      console.log('processed ' + options.name + ': ' + outcome.body.length + ' bytes in ' + outcome.elapsed + ' ms');
      deferred.resolve(outcome);
    });
  });

  req.on('socket', function (res) {
    start = process.hrtime();
  });
  req.on('error', function(e) {
    outcome.elapsed = Math.round(process.hrtime(start)[1] / 1000000);
    outcome.errorCode = e.code;
    deferred.resolve(outcome);
  });

  req.end();

  return deferred.promise;
};

module.exports = {
  execute: execute
}

Spracovanie výsledkov

Keďže je kód na spracovanie asynchrónny, spustia sa všetky requesty naraz, ale odpovede chceme spracovať až keď všetky dobehnú a to z dôvodu šetrenia. Ak zlyhá viacero endpointov, nechceme dostávať jednu správu pre každý zlyhaný endpoint ale jednu SMS so všetkými chybami.

Spravíme si modul notification.js, ktorý bude mať jednu metódu processOutcomes, tá prijíma pole výsledkov (z metódy execute popísanej vyššie).

Táto pracuje v dvoch hlavných krokoch. V prvom iteruje cez všetky výsledky a pripraví pole errors, ktoré obsahuje všetky problémy, ktoré nastali. Nastať môžu nasledujúce problémy:

  • Vrátil sa síce kód 200, ale request trval dlhšie, než sme si nastavili
  • Vrátil sa iný kód než 200
  • Nevrátil sa stavový kód ale nastala iná chyba (napríklad sa nepodarilo resolvovať host name v DNS, ...)

Pra každú chybu si potom pripravíme krátky text obsahujúci názov endpointu a popis chyby. Krátky, aby sme minimalizovali veľkosť SMS správy a neriskovali tak, že sa zbytočne bude odosielať dlhá SMS podelená na viac správ (čo by nás stálo zbytočne viac peňazí).

outcomes.forEach(function(outcome) {
  if(outcome.statusCode && outcome.statusCode == '200') {
    if(outcome.elapsed && outcome.elapsed > outcome.options.responseTimeout) {
      errors.push(outcome.options.name + ': timeout (' + outcome.elapsed + ' ms)')
    }
  } else if (outcome.statusCode) {
    errors.push(outcome.options.name + ': status ' + outcome.statusCode);
  } else {
    errors.push(outcome.options.name + ': error ' + outcome.errorCode);
  }
});

Pokiaľ nastala nejaká chyba, tak ak je povolené Twilio (príznak enabled v nastaveniach), pripravíme SMS a pošleme ju na všetky nakonfigurované čísla. Ak nastala chyba ale Twilio nie je povolené, tak si aspoň vypíšeme chyby do konzoly. A ak chyba nenastala, tak sa z toho patrične tešíme:

if(errors.length) {
  var message = errors.join(' | ');

  if(settings.twilio.enabled) {
    var client = new twilio.RestClient(settings.twilio.accountSid, settings.twilio.authToken);

    settings.twilio.mobiles.forEach(function(mobile) {
      client.sms.messages.create({ to: mobile, from: settings.twilio.number, body: message }, function(error, response) {
        if (error) {
          console.log('Twilio failed: ', error);
        } else {
          console.log('Twilio succeeded: ', response);
        }
      });
    });
  } else {
    console.log('Twilio disabled otherwise this message would have been sent: ' + message);
  }
} else {
  console.log('Yuhu, no errors');
}

Celý modul potom vyzerá nasledovne:

var settings = require('./settings.js');
var twilio = require('twilio');

var processOutcomes = function(outcomes) {
  var errors = [];

  outcomes.forEach(function(outcome) {
    if(outcome.statusCode && outcome.statusCode == '200') {
      if(outcome.elapsed && outcome.elapsed > outcome.options.responseTimeout) {
        errors.push(outcome.options.name + ': timeout (' + outcome.elapsed + ' ms)')
      }
    } else if (outcome.statusCode) {
      errors.push(outcome.options.name + ': status ' + outcome.statusCode);
    } else {
      errors.push(outcome.options.name + ': error ' + outcome.errorCode);
    }
  });

  if(errors.length) {
    var message = errors.join(' | ');

    if(settings.twilio.enabled) {
      var client = new twilio.RestClient(settings.twilio.accountSid, settings.twilio.authToken);

      settings.twilio.mobiles.forEach(function(mobile) {
        client.sms.messages.create({ to: mobile, from: settings.twilio.number, body: message }, function(error, response) {
          if (error) {
            console.log('Twilio failed: ', error);
          } else {
            console.log('Twilio succeeded: ', response);
          }
        });
      });
    } else {
      console.log('Twilio disabled otherwise this message would have been sent: ' + message);
    }
  } else {
    console.log('Yuhu, no errors');
  }
};

module.exports = {
  processOutcomes: processOutcomes
};

Hlavný skript aplikácie

V princípe sme hotoví, ostáva už len funkcionalitu spojiť v hlavnom skripte aplikácie (app.js), teda v prvom rade spustiť kontrolu všetkých endpointov:

var promises = [];
settings.endpoints.forEach(function(item) {
  promises.push(endpoint.execute(item));
});

A následne počkať na resolvnutie všetkých promisov (aby odišla ideálne len jedna SMS) a nechať ich spracovať:

q.allSettled(promises).then(function(results) {
  notification.processOutcomes(results.map(function(item) { return item.value; }));
});

Celý hlavný skript app.js potom vyzerá nasledovne:

var q = require('q');
var settings = require('./settings.js');
var endpoint = require('./endpoint.js');
var notification = require('./notification.js');

var promises = [];

settings.endpoints.forEach(function(item) {
  promises.push(endpoint.execute(item));
});

q.allSettled(promises).then(function(results) {
  notification.processOutcomes(results.map(function(item) { return item.value; }));
});

Ako bolo vidieť, použili sme dva externé moduly, ktoré sme si samozrejme nainštalovali cez npm, hneď po tom, ako sme si npm inicializovali:

npm init
npm install q --save
npm install twilio --save

Pravideľné spúšťanie na Raspberry Pi cez cron

Samotná mini aplikácia je super, ale neužitočná v prípade, že ju nespúšťate pravideľne. Takže ju nezabudnite spúšťať v pravideľných intervaloch, vo dne v noci, dobre?

Čože? Aha, že sa vám nechce spúsťať ručne? Samozrejme že nie, ale na to máte Raspberry pi alebo iný *nixový server s nainštalovaným Node.js a prístupným cron-om.

Nebudem tu popisovať detailne ako sa nastavuje cron, ale ukážem vám aspoň základy. Čo máte v cron tabuľke si viete skontrolovať nasledujúcim príkazom:

crontab -l

V tabuľke si viete definovať čo a kedy má byť spúšťané, možnosti sú veľmi flexibilné. My si nastavíme, aby sa skript spúšťal každých 5 minút, to nám stačí (teda mne, vám možno nie). Spustite tento príkaz, aby sa vám otvoril editor s cron tabuľkou:

crontab -e

Vykonajte zmeny, ktoré chcete a potom ich uložte, netrápte sa cestou k súboru, ani tým, že vám ponúka súbor v tmp adresári. Po uložení zmien si súbor skontrolujte (cez crontab -l) a uvidíte, že zmeny sú korektne uložené.

V našom príklade si nastavíme, aby sa každých 5 minút spúšťal skript app.js z adresára /home/jasam/monitoring. Aj na takúto aplikáciu môžete samozrejme použiť Git deployment, ktorý som popísal minule. Pri editácii crontab pridajte na nový riadok (ktorý nesmie začínať znakom #, ten totiž funguje na zakomentovanie riadku):

*/5 * * * * /usr/local/bin/node /home/jasam/monitoring/app.js

Po uložení zmien sa vám bude skript spúsťať a kontrolovať vaše weby. A ak budete chcieť kontrolu vypnúť? Jednoducho tento riadok zase odstráňte, alebo zakomentujte (pridaním znaku # na začiatok riadku).

Šikovné nie? Pozrieť si celý zdrojový kód môžete na Githube.

Mohlo by ťa tiež zaujímať

Páčil sa ti príspevok?

Zdieľaj príspevok alebo si ho odlož na neskôr

Sleduj ma

Ak nechceš premeškať príspevky ako je tento, sleduj ma na Twitteri, alebo ak máš RSS čítačku, môžeš sledovať môj RSS kanál.