http-check-controller.class.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import axios, { AxiosError, AxiosRequestConfig } from 'axios';
  2. import moment from 'moment';
  3. import defaults from '../../../common/defaults.module';
  4. import { HttpCheckStatus } from '../../../common/lib/http-check-data.module';
  5. import { Logger } from '../../../common/util/logger.class';
  6. import { Timer } from '../timer.class';
  7. import { Database, ServiceChangedStatus } from './database.class';
  8. import { FCMController } from './fcm-controller.class';
  9. const FCM_TOPIC_SERVICES = 'monitoring-services';
  10. type Subscriber = { id: number; interval: number; conf: HttpCheckConfig };
  11. export class HttpCheckController {
  12. private subscriptions: Array<Subscriber> = [];
  13. private db!: Database;
  14. constructor() {
  15. this.db = new Database();
  16. (async () => {
  17. try {
  18. await this.db.open();
  19. const configs = await this.db.getHttpCheckConfigs();
  20. for (const conf of configs) {
  21. if (!conf) return;
  22. if (!conf.active) return;
  23. await this.scheduleCheck(conf);
  24. Logger.info('[INFO] Initial HTTP Service Check for', conf.title, '...');
  25. await this.timerTick(conf);
  26. }
  27. } catch (err) {
  28. Logger.error('[FATAL] Initializing ServerConnector failed:', err);
  29. Logger.error('[EXITING]');
  30. process.exit(1);
  31. }
  32. })();
  33. }
  34. async updateCheck(status: ServiceChangedStatus, conf: HttpCheckConfig) {
  35. const subscriber = this.subscriptions.find(sub => sub.conf.id === conf.id);
  36. switch (status) {
  37. case ServiceChangedStatus.Created:
  38. case ServiceChangedStatus.Activated:
  39. await this.scheduleCheck(conf);
  40. break;
  41. case ServiceChangedStatus.Deactivated:
  42. await this.unscheduleCheck(subscriber);
  43. break;
  44. case ServiceChangedStatus.Rescheduled:
  45. await this.rescheduleCheck(conf, subscriber);
  46. break;
  47. default:
  48. break;
  49. }
  50. }
  51. private async scheduleCheck(conf: HttpCheckConfig, log = true) {
  52. let interval = Number(conf.interval);
  53. if (Number.isNaN(interval)) interval = defaults.serviceChecks.interval;
  54. if (log) Logger.info(`[INFO] Starting HTTP Service Check Controller for "${conf.title}" with interval ${interval} seconds ...`);
  55. const id = Timer.instance.subscribe(interval, async () => await this.timerTick(conf));
  56. const sub = { id, interval, conf };
  57. this.subscriptions.push(sub);
  58. return sub;
  59. }
  60. private async rescheduleCheck(conf: HttpCheckConfig, sub?: Subscriber) {
  61. Logger.info('[INFO] Rescheduling HTTP Service Check for', conf.title);
  62. await this.unscheduleCheck(sub, false);
  63. await this.scheduleCheck(conf, false);
  64. }
  65. private async unscheduleCheck(sub?: Subscriber, log = true) {
  66. if (!sub) return;
  67. if (log) Logger.info('[INFO] Removing HTTP Service Check for', sub.conf.title);
  68. Timer.instance.unsubscribe(sub.id);
  69. this.subscriptions = this.subscriptions.filter(s => s.id !== sub.id);
  70. }
  71. private async timerTick(conf: HttpCheckConfig) {
  72. Logger.debug('[DEBUG] TICK', new Date(), JSON.stringify(conf));
  73. const now = new Date();
  74. const options: AxiosRequestConfig<any> = {
  75. timeout: conf.timeout,
  76. responseType: 'text'
  77. };
  78. let success = true;
  79. try {
  80. const id = conf.id;
  81. conf = (await this.db.getHttpCheckConfigByID(conf.serverId ?? 0, id)) as HttpCheckConfig;
  82. if (!conf) {
  83. Logger.warn(`[WARN] HealthCheckConfig(${id}) not found in Database but still scheduled in Timer!`);
  84. return;
  85. }
  86. options.timeout = conf.timeout;
  87. let response = await axios.get(conf.url, options);
  88. const responseText = new String(response.data).toString();
  89. for (const check of conf.checks) {
  90. const reg = new RegExp(check, 'i');
  91. if (!reg.test(responseText)) {
  92. Logger.debug(`[DEBUG] Regular expression /${check}/i not found in response`);
  93. await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.CheckFailed, `Regular expression /${check}/i not found in response`);
  94. success = false;
  95. }
  96. }
  97. if (success) {
  98. Logger.debug(`[DEBUG] HTTP Service Check "${conf.title}": OK.`);
  99. await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.OK, 'OK');
  100. }
  101. } catch (err) {
  102. let log = false;
  103. success = false;
  104. if (err instanceof AxiosError) {
  105. // err.code = 'ECONNREFUSED' | 'ECONNABORTED' | 'ERR_BAD_REQUEST' | 'ERR_BAD_RESPONSE' | ...?
  106. try {
  107. if (err.code === 'ECONNABORTED') {
  108. await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.Timeout, err.message);
  109. } else if (err.code && ['ERR_BAD_REQUEST', 'ERR_BAD_RESPONSE'].includes(err.code)) {
  110. await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.RequestFailed, `${err.response?.status} ${err.response?.statusText}`);
  111. } else {
  112. await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.RequestFailed, err.message);
  113. }
  114. } catch (insertErr) {
  115. Logger.error(`[ERROR] Inserting HealthCheckData on Error failed:`, insertErr);
  116. log = true;
  117. }
  118. } else {
  119. try {
  120. await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.Unknown, new String(err).toString());
  121. } catch (insertErr) {
  122. Logger.error(`[ERROR] Inserting HealthCheckData on Error failed:`, insertErr);
  123. }
  124. log = true;
  125. }
  126. if (log) Logger.error('[ERROR] HTTP Service Check failed:', err);
  127. }
  128. if (!success && conf.notify) {
  129. try {
  130. const lastErrors = await this.db.getLastErrors(conf.id, conf.notifyThreshold + 1);
  131. if (lastErrors.length > conf.notifyThreshold) {
  132. Logger.debug(`[DEBUG] Sending FCM Notification for`, conf.title);
  133. const lastCheck = lastErrors[0];
  134. const lastError = lastCheck.data[0];
  135. await FCMController.instance.sendNotificationToTopic(FCM_TOPIC_SERVICES, {
  136. title: `[CRIT] ${conf.title} since ${moment(lastCheck.time).format('HH:mm')}`,
  137. body:
  138. `HTTP Check '${conf.title}' has failed over ${conf.notifyThreshold} times in a row\n` +
  139. `Last error status was: (${lastError.status}) ${lastError.message}`
  140. });
  141. }
  142. } catch (err) {
  143. Logger.error('[ERROR] Notification failure:', err);
  144. }
  145. }
  146. }
  147. async close() {
  148. if (!this.db) return;
  149. await this.db.close();
  150. }
  151. }