services-api-handler.class.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { RouterOptions, json } from 'express';
  2. import { HttpCheckData, HttpCheckStatus, ServiceCheckData, ServiceCheckDataEntry } from '../../../common/lib/http-check-data.module';
  3. import { HttpStatusException } from '../../../common/lib/http-status.exception';
  4. import { ControllerPool } from '../ctrl/controller-pool.interface';
  5. import { HealthCheckDataProvider } from '../ctrl/health-check-data-provider.interface';
  6. import { ServiceChangedStatus } from '../lib/service-changed-status.enum';
  7. import { WebHandler } from './web-handler.base';
  8. export class ServicesAPIHandler extends WebHandler {
  9. constructor(protected ctrlPool: ControllerPool, options?: RouterOptions) {
  10. super(ctrlPool, options);
  11. this.router.use(json());
  12. this.router.use(this.avoidCache);
  13. this.router.get('/:serverID', async (req, res, next) => {
  14. try {
  15. const serverID = this.validateNumber(req.params.serverID, 'server id');
  16. const services = await req.db.getHttpCheckConfigs(serverID);
  17. res.send(services);
  18. } catch (err) {
  19. next(err);
  20. }
  21. });
  22. this.router.put('/:serverID', async (req, res, next) => {
  23. try {
  24. const serverID = this.validateNumber(req.params.serverID, 'server id');
  25. const result = await req.db.saveHttpCheckConfig(serverID, req.body);
  26. if (result.status !== ServiceChangedStatus.None) {
  27. await this.ctrlPool.httpChecks.updateCheck(result.status, result.result);
  28. }
  29. res.send(result.result);
  30. } catch (err) {
  31. next(err);
  32. }
  33. });
  34. this.router.delete('/:serverID/:serviceID', async (req, res, next) => {
  35. try {
  36. const serverID = this.validateNumber(req.params.serverID, 'server id');
  37. const serviceID = this.validateNumber(req.params.serviceID, 'service id');
  38. const deleted = await req.db.deleteHealthCheckConfig(serverID, serviceID);
  39. if (deleted) {
  40. await this.ctrlPool.httpChecks.updateCheck(ServiceChangedStatus.Deactivated, { id: serviceID } as HttpCheckConfig);
  41. }
  42. res.send({ deleted });
  43. } catch (err) {
  44. next(err);
  45. }
  46. });
  47. this.router.get('/:serverID/:serviceID', async (req, res, next) => {
  48. try {
  49. const serverID = this.validateNumber(req.params.serverID, 'server id');
  50. const serviceID = this.validateNumber(req.params.serviceID, 'service id');
  51. const result = await req.db.getHttpCheckConfigByID(serverID, serviceID);
  52. res.send(result);
  53. } catch (err) {
  54. next(err);
  55. }
  56. });
  57. this.router.get('/:serverID/:serviceID/data', async (req, res, next) => {
  58. try {
  59. const serverID = this.validateNumber(req.params.serverID, 'server id');
  60. const serviceID = this.validateNumber(req.params.serviceID, 'service id');
  61. const qStart = (req.query.start || '').toString();
  62. const qEnd = (req.query.end || '').toString();
  63. if (!qStart || !qEnd) throw new HttpStatusException("QueryParams 'start' and 'end' are mandatory.", 400);
  64. const start = new Date(qStart);
  65. const end = new Date(qEnd);
  66. if ([start.toString(), end.toString()].includes('Invalid Date')) {
  67. throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
  68. }
  69. const data = await req.db.queryServiceCheckData(serverID, serviceID, start, end);
  70. res.send({
  71. start,
  72. end,
  73. data
  74. } as QueryResponse<ServiceCheckData[]>);
  75. } catch (err) {
  76. next(err);
  77. }
  78. });
  79. this.router.get('/:serverID/:serviceID/logs', async (req, res, next) => {
  80. try {
  81. const serverID = this.validateNumber(req.params.serverID, 'server id');
  82. const serviceID = this.validateNumber(req.params.serviceID, 'service id');
  83. const qStart = (req.query.start || '').toString();
  84. const qEnd = (req.query.end || '').toString();
  85. if (!qStart || !qEnd) throw new HttpStatusException("QueryParams 'start' and 'end' are mandatory.", 400);
  86. const start = new Date(qStart);
  87. const end = new Date(qEnd);
  88. if ([start.toString(), end.toString()].includes('Invalid Date')) {
  89. throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
  90. }
  91. const data = await req.db.queryServiceCheckLogs(serverID, serviceID, start, end);
  92. res.send({
  93. start,
  94. end,
  95. data
  96. } as QueryResponse<ServiceCheckData[]>);
  97. } catch (err) {
  98. next(err);
  99. }
  100. });
  101. this.router.post('/test', async (req, res, next) => {
  102. try {
  103. const config = req.body as HttpCheckConfig;
  104. const mockDB = new HealthCheckDatabaseMock(config);
  105. await this.ctrlPool.httpChecks.runCheck(config, mockDB);
  106. res.send(mockDB.log);
  107. } catch (err) {
  108. next(err);
  109. }
  110. });
  111. }
  112. private validateNumber(id: string, field: string) {
  113. const num = Number(id);
  114. if (Number.isNaN(num)) {
  115. throw new HttpStatusException(`Not a valid ${field}: ${id}`, 400);
  116. }
  117. return num;
  118. }
  119. }
  120. class HealthCheckDatabaseMock implements HealthCheckDataProvider {
  121. public log: HttpCheckData[] = [];
  122. constructor(private config: HttpCheckConfig) {}
  123. async open() {}
  124. async getHttpCheckConfigByID(serverID: number, configID: number) {
  125. return this.config;
  126. }
  127. async insertHealthCheckData(confID: number, time: Date, status: HttpCheckStatus, message: string) {
  128. const logEntry = { configId: confID, id: new Date().getTime(), time, status, message };
  129. this.log.push(logEntry);
  130. return logEntry;
  131. }
  132. async getLastErrors(confID: number, threshold: number) {
  133. if (this.log.length === 0) return [];
  134. const mapByTimestamp = new Map<number, ServiceCheckDataEntry[]>();
  135. mapByTimestamp.set(this.log[0].time.getTime(), this.log);
  136. const errors: ServiceCheckData[] = [];
  137. for (const entry of mapByTimestamp.entries()) {
  138. const time = entry[0];
  139. const data = entry[1];
  140. const errorData = data.filter(d => d.status !== HttpCheckStatus.OK);
  141. if (!errorData.length) break;
  142. errors.push({
  143. time: new Date(time),
  144. data: errorData
  145. });
  146. }
  147. return errors;
  148. }
  149. async close() {}
  150. }