import { RouterOptions, json } from 'express'; import { HttpCheckData, HttpCheckStatus, ServiceCheckData, ServiceCheckDataEntry } from '../../../common/lib/http-check-data.module'; import { HttpStatusException } from '../../../common/lib/http-status.exception'; import { ControllerPool } from '../ctrl/controller-pool.interface'; import { HealthCheckDataProvider } from '../ctrl/health-check-data-provider.interface'; import { ServiceChangedStatus } from '../lib/service-changed-status.enum'; import { WebHandler } from './web-handler.base'; export class ServicesAPIHandler extends WebHandler { constructor(protected ctrlPool: ControllerPool, options?: RouterOptions) { super(ctrlPool, options); this.router.use(json()); this.router.use(this.avoidCache); this.router.get('/:serverID', async (req, res, next) => { try { const serverID = this.validateNumber(req.params.serverID, 'server id'); const services = await req.db.getHttpCheckConfigs(serverID); res.send(services); } catch (err) { next(err); } }); this.router.put('/:serverID', async (req, res, next) => { try { const serverID = this.validateNumber(req.params.serverID, 'server id'); const result = await req.db.saveHttpCheckConfig(serverID, req.body); if (result.status !== ServiceChangedStatus.None) { await this.ctrlPool.httpChecks.updateCheck(result.status, result.result); } res.send(result.result); } catch (err) { next(err); } }); this.router.delete('/:serverID/:serviceID', async (req, res, next) => { try { const serverID = this.validateNumber(req.params.serverID, 'server id'); const serviceID = this.validateNumber(req.params.serviceID, 'service id'); const deleted = await req.db.deleteHealthCheckConfig(serverID, serviceID); if (deleted) { await this.ctrlPool.httpChecks.updateCheck(ServiceChangedStatus.Deactivated, { id: serviceID } as HttpCheckConfig); } res.send({ deleted }); } catch (err) { next(err); } }); this.router.get('/:serverID/:serviceID', async (req, res, next) => { try { const serverID = this.validateNumber(req.params.serverID, 'server id'); const serviceID = this.validateNumber(req.params.serviceID, 'service id'); const result = await req.db.getHttpCheckConfigByID(serverID, serviceID); res.send(result); } catch (err) { next(err); } }); this.router.get('/:serverID/:serviceID/data', async (req, res, next) => { try { const serverID = this.validateNumber(req.params.serverID, 'server id'); const serviceID = this.validateNumber(req.params.serviceID, 'service id'); const qStart = (req.query.start || '').toString(); const qEnd = (req.query.end || '').toString(); if (!qStart || !qEnd) throw new HttpStatusException("QueryParams 'start' and 'end' are mandatory.", 400); const start = new Date(qStart); const end = new Date(qEnd); if ([start.toString(), end.toString()].includes('Invalid Date')) { throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400); } const data = await req.db.queryServiceCheckData(serverID, serviceID, start, end); res.send({ start, end, data } as QueryResponse); } catch (err) { next(err); } }); this.router.get('/:serverID/:serviceID/logs', async (req, res, next) => { try { const serverID = this.validateNumber(req.params.serverID, 'server id'); const serviceID = this.validateNumber(req.params.serviceID, 'service id'); const qStart = (req.query.start || '').toString(); const qEnd = (req.query.end || '').toString(); if (!qStart || !qEnd) throw new HttpStatusException("QueryParams 'start' and 'end' are mandatory.", 400); const start = new Date(qStart); const end = new Date(qEnd); if ([start.toString(), end.toString()].includes('Invalid Date')) { throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400); } const data = await req.db.queryServiceCheckLogs(serverID, serviceID, start, end); res.send({ start, end, data } as QueryResponse); } catch (err) { next(err); } }); this.router.post('/test', async (req, res, next) => { try { const config = req.body as HttpCheckConfig; const mockDB = new HealthCheckDatabaseMock(config); await this.ctrlPool.httpChecks.runCheck(config, mockDB); res.send(mockDB.log); } catch (err) { next(err); } }); } private validateNumber(id: string, field: string) { const num = Number(id); if (Number.isNaN(num)) { throw new HttpStatusException(`Not a valid ${field}: ${id}`, 400); } return num; } } class HealthCheckDatabaseMock implements HealthCheckDataProvider { public log: HttpCheckData[] = []; constructor(private config: HttpCheckConfig) {} async open() {} async getHttpCheckConfigByID(serverID: number, configID: number) { return this.config; } async insertHealthCheckData(confID: number, time: Date, status: HttpCheckStatus, message: string) { const logEntry = { configId: confID, id: new Date().getTime(), time, status, message }; this.log.push(logEntry); return logEntry; } async getLastErrors(confID: number, threshold: number) { if (this.log.length === 0) return []; const mapByTimestamp = new Map(); mapByTimestamp.set(this.log[0].time.getTime(), this.log); const errors: ServiceCheckData[] = []; for (const entry of mapByTimestamp.entries()) { const time = entry[0]; const data = entry[1]; const errorData = data.filter(d => d.status !== HttpCheckStatus.OK); if (!errorData.length) break; errors.push({ time: new Date(time), data: errorData }); } return errors; } async close() {} }