diff --git a/cron.json b/cron.json index 95208f0..fdd883e 100644 --- a/cron.json +++ b/cron.json @@ -3,6 +3,10 @@ { "command": "*/10 * * * * npm run script:checkSystemPulses", "size": "S" + }, + { + "command": "*/10 * * * * npm run script:pingMonitors", + "size": "S" } ] } diff --git a/package.json b/package.json index 0993295..3eb785e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "test:back": "jest", "typeorm": "typeorm-ts-node-esm", "script:importDb": "npm run buildServer && node dist/src/scripts/importDb.js", - "script:checkSystemPulses": "npm run buildServer && node dist/src/scripts/checkSystemPulses.js" + "script:checkSystemPulses": "npm run buildServer && node dist/src/scripts/checkSystemPulses.js", + "script:pingMonitors": "npm run buildServer && node dist/src/scripts/pingMonitors.js" }, "author": "", "license": "ISC", diff --git a/src/dataSource.ts b/src/dataSource.ts index 9b8e937..4997434 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -4,6 +4,7 @@ import { config } from './config'; import { SystemPulse } from './modules/systemPulse'; import { Event } from './modules/event'; import { Monitor } from './modules/monitor'; +import { MonitorEvent } from './modules/monitorEvent'; const dataSource = new DataSource({ type: 'postgres', @@ -14,7 +15,7 @@ const dataSource = new DataSource({ database: config.DATABASE_NAME, logging: ['warn', 'error'], connectTimeoutMS: 20000, - entities: [SystemPulse, Event, Monitor], + entities: [SystemPulse, Event, Monitor, MonitorEvent], subscribers: [], migrations: ['**/migrations/*.js'], }); diff --git a/src/migrations/1733506046286-add-last-call.ts b/src/migrations/1733506046286-add-last-call.ts new file mode 100644 index 0000000..30db11c --- /dev/null +++ b/src/migrations/1733506046286-add-last-call.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddLastCall1733506046286 implements MigrationInterface { + name = 'AddLastCall1733506046286' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monitor" ADD "lastCall" TIMESTAMP`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monitor" DROP COLUMN "lastCall"`); + } + +} diff --git a/src/modules/genericEvent/types.ts b/src/modules/genericEvent/types.ts new file mode 100644 index 0000000..cc285c8 --- /dev/null +++ b/src/modules/genericEvent/types.ts @@ -0,0 +1,16 @@ +interface genericEvent { + id: number; + + title: string; + + kind: eventKindType; + + createdAt: string; +} + +const EVENT_KINDS = ['up', 'down'] as const; + +type eventKindType = (typeof EVENT_KINDS)[number]; + +export { EVENT_KINDS }; +export type { eventKindType, genericEvent }; diff --git a/src/modules/monitor/Monitor.entity.ts b/src/modules/monitor/Monitor.entity.ts index bc13aab..b205374 100644 --- a/src/modules/monitor/Monitor.entity.ts +++ b/src/modules/monitor/Monitor.entity.ts @@ -16,4 +16,7 @@ export class Monitor { @Column({ type: 'timestamp', nullable: true }) lastSuccessfulCall: string | null; + + @Column({ type: 'timestamp', nullable: true }) + lastCall: string | null; } diff --git a/src/modules/monitor/index.ts b/src/modules/monitor/index.ts index ecb3e3f..aa93966 100644 --- a/src/modules/monitor/index.ts +++ b/src/modules/monitor/index.ts @@ -1,4 +1,5 @@ import { Monitor } from './Monitor.entity'; import { buildMonitorController } from './monitor.controller'; +import { buildMonitorService } from './monitor.service'; -export { Monitor, buildMonitorController }; +export { Monitor, buildMonitorController, buildMonitorService }; diff --git a/src/modules/monitor/monitor.service.ts b/src/modules/monitor/monitor.service.ts index 74745ce..5eba0f7 100644 --- a/src/modules/monitor/monitor.service.ts +++ b/src/modules/monitor/monitor.service.ts @@ -1,4 +1,5 @@ import { dataSource } from '../../dataSource'; +import { buildMonitorEventService } from '../monitorEvent'; import { Monitor } from './Monitor.entity'; export { buildMonitorService }; @@ -7,6 +8,9 @@ function buildMonitorService() { const monitorRepository = dataSource.getRepository(Monitor); const monitorService = { createMonitor, + getMonitors, + computeShouldPingMonitor, + pingMonitor, }; return monitorService; @@ -23,4 +27,61 @@ function buildMonitorService() { }); return { monitorId: result.identifiers[0].id }; } + + async function getMonitors() { + return monitorRepository.find({}); + } + + async function pingMonitor(monitor: Monitor) { + const monitorEventService = buildMonitorEventService(); + await monitorRepository.update({ id: monitor.id }, { lastCall: () => 'CURRENT_TIMESTAMP' }); + const lastMonitorEvent = await monitorEventService.getLastMonitorEvent(monitor.id); + + try { + await fetch(monitor.url); + await monitorRepository.update( + { id: monitor.id }, + { lastSuccessfulCall: () => 'CURRENT_TIMESTAMP' }, + ); + if (!lastMonitorEvent) { + return monitorEventService.createMonitorEvent(monitor.id, { + title: 'Service en route !', + kind: 'up', + }); + } else { + if (lastMonitorEvent.kind === 'down') { + await monitorEventService.createMonitorEvent(monitor.id, { + title: 'Le service est revenu', + kind: 'up', + }); + } + } + } catch (error) { + console.error(error); + if (!lastMonitorEvent) { + return monitorEventService.createMonitorEvent(monitor.id, { + title: 'Service en panne...', + kind: 'down', + }); + } else { + if (lastMonitorEvent.kind === 'up') { + await monitorEventService.createMonitorEvent(monitor.id, { + title: 'Le service est tombé !', + kind: 'down', + }); + } + } + } + } + + function computeShouldPingMonitor(monitor: Monitor, now: Date) { + if (monitor.lastCall === null) { + return true; + } + const lastCallDate = new Date(monitor.lastCall); + if (now.getTime() - lastCallDate.getTime() > monitor.frequency * 60 * 1000) { + return true; + } + return false; + } } diff --git a/src/modules/monitorEvent/MonitorEvent.entity.ts b/src/modules/monitorEvent/MonitorEvent.entity.ts new file mode 100644 index 0000000..3b2f020 --- /dev/null +++ b/src/modules/monitorEvent/MonitorEvent.entity.ts @@ -0,0 +1,21 @@ +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Monitor } from '../monitor'; +import { EVENT_KINDS, eventKindType, genericEvent } from '../genericEvent/types'; + +@Entity() +export class MonitorEvent implements genericEvent { + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @Column('enum', { enum: EVENT_KINDS }) + kind: eventKindType; + + @ManyToOne(() => Monitor, { onDelete: 'CASCADE', nullable: false }) + monitor: Monitor; + + @CreateDateColumn({ type: 'timestamp' }) + createdAt: string; +} diff --git a/src/modules/monitorEvent/index.ts b/src/modules/monitorEvent/index.ts new file mode 100644 index 0000000..aa681d4 --- /dev/null +++ b/src/modules/monitorEvent/index.ts @@ -0,0 +1,3 @@ +import { MonitorEvent } from './MonitorEvent.entity'; +import { buildMonitorEventService } from './monitorEvent.service'; +export { MonitorEvent, buildMonitorEventService }; diff --git a/src/modules/monitorEvent/monitorEvent.service.ts b/src/modules/monitorEvent/monitorEvent.service.ts new file mode 100644 index 0000000..9c70fde --- /dev/null +++ b/src/modules/monitorEvent/monitorEvent.service.ts @@ -0,0 +1,36 @@ +import { dataSource } from '../../dataSource'; +import { Monitor } from '../monitor'; +import { MonitorEvent } from './MonitorEvent.entity'; + +export { buildMonitorEventService }; + +function buildMonitorEventService() { + const monitorMonitorEventRepository = dataSource.getRepository(MonitorEvent); + + const monitorMonitorEventService = { + createMonitorEvent, + getLastMonitorEvent, + }; + + return monitorMonitorEventService; + + async function getLastMonitorEvent(monitorId: Monitor['id']) { + return monitorMonitorEventRepository.findOne({ + where: { monitor: { id: monitorId } }, + order: { createdAt: 'DESC' }, + relations: ['monitor'], + }); + } + + async function createMonitorEvent( + monitorId: Monitor['id'], + params: { title: MonitorEvent['title']; kind: MonitorEvent['kind'] }, + ) { + await monitorMonitorEventRepository.insert({ + monitor: { id: monitorId }, + title: params.title, + kind: params.kind, + }); + return true; + } +} diff --git a/src/scripts/pingMonitors.ts b/src/scripts/pingMonitors.ts new file mode 100644 index 0000000..8960387 --- /dev/null +++ b/src/scripts/pingMonitors.ts @@ -0,0 +1,26 @@ +import { dataSource } from '../dataSource'; +import { buildMonitorService } from '../modules/monitor'; + +async function pingMonitors() { + const monitorService = buildMonitorService(); + console.log('Initializing database...'); + await dataSource.initialize(); + console.log('Database initialized!'); + console.log('Getting monitors...'); + const monitors = await monitorService.getMonitors(); + console.log(`${monitors.length} monitors found!`); + console.log('Pinging monitors...'); + await Promise.all( + monitors.map(async (monitor) => { + const shouldPingMonitor = monitorService.computeShouldPingMonitor(monitor, new Date()); + if (!shouldPingMonitor) { + return; + } + return monitorService.pingMonitor(monitor); + }), + ); + + console.log('Done!'); +} + +pingMonitors();