sqlite-database.class.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import fs from 'fs';
  2. import fsp from 'fs/promises';
  3. import moment from 'moment';
  4. import path from 'path';
  5. import { Database as SQLiteDB, OPEN_CREATE, OPEN_READWRITE } from 'sqlite3';
  6. import defaults from '../../../common/defaults.module';
  7. import { ServiceConfig, validateParamType } from '../../../common/interfaces/service-config.interface';
  8. import { HttpCheckData, HttpCheckStatus, ServiceCheckData, ServiceCheckDataEntry } from '../../../common/lib/http-check-data.module';
  9. import { Logger } from '../../../common/util/logger.class';
  10. import { ServiceChangedStatus } from '../lib/service-changed-status.enum';
  11. import { ValidationException } from '../lib/validation-exception.class';
  12. import { DataProvider } from './data-provider.interface';
  13. import { DBMigration } from './db-migration.class';
  14. import { SQLiteController } from './sqlite-controller.base';
  15. export class SQLiteDatabase extends SQLiteController implements DataProvider {
  16. public set onError(listener: (error: any) => void) {
  17. this._onError = listener;
  18. }
  19. private _onError: (error: any) => void = err => console.error('[DB.ONERROR]', err);
  20. public async open(migrate = false) {
  21. try {
  22. const DATA_DIR = process.env.DATA_DIR || 'data';
  23. if (!fs.existsSync(DATA_DIR)) await fsp.mkdir(DATA_DIR);
  24. const DATA_FILE = path.resolve(DATA_DIR, 'data.db');
  25. const exists = fs.existsSync(DATA_FILE);
  26. await new Promise<void>((res, rej) => {
  27. this.db = new SQLiteDB(DATA_FILE, OPEN_READWRITE | OPEN_CREATE, err => (err ? rej(err) : res()));
  28. this.db.on('error', this._onError);
  29. });
  30. Logger.info('[INFO]', exists ? 'Opened' : 'Created', 'SQLite3 Database file', DATA_FILE);
  31. if (!this.db) throw new Error('Database not opened.');
  32. if (!exists) {
  33. // INITIAL TABLE SETUP
  34. await this.run(
  35. `CREATE TABLE Server (
  36. ID INTEGER PRIMARY KEY AUTOINCREMENT,
  37. Title TEXT NOT NULL UNIQUE,
  38. FQDN TEXT NOT NULL UNIQUE
  39. );`,
  40. []
  41. );
  42. await this.run(
  43. `CREATE TABLE ServerConfig (
  44. ID INTEGER PRIMARY KEY AUTOINCREMENT,
  45. ServerID INTEGER NOT NULL,
  46. Key TEXT NOT NULL,
  47. Value TEXT NOT NULL,
  48. FOREIGN KEY(ServerID) REFERENCES Server(ID),
  49. UNIQUE(ServerID, Key)
  50. )`,
  51. []
  52. );
  53. await this.run(
  54. `CREATE TABLE ServerDataEntry (
  55. ID INTEGER PRIMARY KEY AUTOINCREMENT,
  56. ServerID INTEGER NOT NULL,
  57. Timestamp INTEGER NOT NULL,
  58. FOREIGN KEY(ServerID) REFERENCES Server(ID),
  59. UNIQUE(ServerID, Timestamp)
  60. );`,
  61. []
  62. );
  63. await this.run(
  64. `CREATE TABLE ServerDataValue (
  65. ID INTEGER PRIMARY KEY AUTOINCREMENT,
  66. EntryID INTEGER NOT NULL,
  67. Type Text NOT NULL,
  68. Key TEXT NOT NULL,
  69. Value REAL NOT NULL,
  70. FOREIGN KEY(EntryID) REFERENCES ServerDataEntry(ID),
  71. UNIQUE(EntryID, Type, Key)
  72. );`,
  73. []
  74. );
  75. let result = await this.run(`INSERT INTO Server(Title, FQDN) VALUES(?, ?);`, ['Raspi4', '10.8.0.10']);
  76. const serverID = result.lastID;
  77. Logger.debug(`[DEBUG] Created Server #${serverID}`);
  78. result = await this.run(`INSERT INTO ServerConfig(ServerID, Key, Value) VALUES(?, ?, ?);`, [serverID, 'syncInterval', 300]);
  79. }
  80. if (migrate) {
  81. // RUN DB MIGRATIONS
  82. const mig = new DBMigration(this.db);
  83. await mig.update();
  84. }
  85. // DB RUNTIME SETTINGS
  86. await this.exec('PRAGMA foreign_keys=on;');
  87. } catch (err) {
  88. Logger.error('[FATAL] Initializing Database failed:', err);
  89. Logger.error('[EXITING]');
  90. process.exit(1);
  91. }
  92. }
  93. public async getAllServerConfigs(): Promise<Server[]> {
  94. const res = await this.stmt(
  95. `SELECT
  96. Server.*,
  97. ServerConfig.Key,
  98. ServerConfig.Value
  99. FROM Server
  100. LEFT OUTER JOIN ServerConfig ON Server.ID = ServerConfig.ServerID
  101. ORDER BY Server.Title, ServerConfig.Key`,
  102. []
  103. );
  104. return res.rows.reduce((res: Server[], line, i) => {
  105. const serverID = line['ID'];
  106. let server: Server;
  107. if (i === 0 || res[res.length - 1].id !== serverID) {
  108. server = { id: serverID, title: line['Title'], fqdn: line['FQDN'], config: {} };
  109. res.push(server);
  110. } else {
  111. server = res[res.length - 1];
  112. }
  113. if (!!line['Key']) {
  114. server.config[line['Key']] = line['Value'];
  115. }
  116. return res;
  117. }, [] as Server[]);
  118. }
  119. public async insertServerData(serverID: number, data: ReducedData[]) {
  120. if (!data.length) return;
  121. await this.beginTransaction();
  122. try {
  123. for (const entry of data) {
  124. const result = await this.run('INSERT INTO ServerDataEntry(ServerID, Timestamp) VALUES(?, ?);', [serverID, entry.time.getTime()]);
  125. let entryID = result.lastID;
  126. for (const type of Object.keys(entry).filter(t => !['time', 'hdd'].includes(t))) {
  127. for (const key of Object.keys((entry as any)[type])) {
  128. await this.run('INSERT INTO ServerDataValue(EntryID, Type, Key, Value) VALUES(?, ?, ?, ?);', [
  129. entryID,
  130. type,
  131. key,
  132. (entry as any)[type][key]
  133. ]);
  134. }
  135. }
  136. if (entry.hdd) {
  137. for (const mount of Object.keys(entry.hdd)) {
  138. for (const key of Object.keys(entry.hdd[mount])) {
  139. await this.run('INSERT INTO ServerDataValue(EntryID, Type, Key, Value) VALUES(?, ?, ?, ?);', [
  140. entryID,
  141. `hdd:${mount}`,
  142. key,
  143. (entry.hdd[mount] as any)[key]
  144. ]);
  145. }
  146. }
  147. }
  148. }
  149. await this.commit();
  150. } catch (err) {
  151. await this.rollback();
  152. throw err;
  153. }
  154. }
  155. public async getServerDataTypes(serverID: number) {
  156. const results = await this.stmt(
  157. `
  158. SELECT
  159. ServerDataValue.Type
  160. FROM ServerDataEntry
  161. JOIN ServerDataValue ON ServerDataEntry.ID = ServerDataValue.EntryID
  162. WHERE ServerDataEntry.ServerID = ?
  163. GROUP BY ServerDataValue.Type
  164. ORDER BY ServerDataValue.Type;
  165. `,
  166. [serverID]
  167. );
  168. return results.rows.reduce((res: Array<ServerDataTypesConfig>, { Type: type }) => {
  169. if (!type.startsWith('hdd:')) {
  170. res.push({ type });
  171. } else {
  172. let hdd = res.find(c => c.type === 'hdd');
  173. if (!hdd) {
  174. hdd = { type: 'hdd', subtypes: [] };
  175. res.push(hdd);
  176. }
  177. hdd.subtypes?.push({ type: type.substring(4) });
  178. }
  179. return res;
  180. }, []) as Array<ServerDataTypesConfig>;
  181. }
  182. public async queryServerData(serverID: number, type: ServerDataType, from: Date, to: Date): Promise<ServerData[]> {
  183. const diffMs = moment(to).diff(moment(from));
  184. const sectionMs = Math.floor(diffMs / 100);
  185. const select_max = type !== 'cpu';
  186. const select_types = select_max ? [type, type, type] : [type, type];
  187. const result = await this.stmt(
  188. `
  189. SELECT
  190. CEIL(Timestamp / ?) * ? as 'Timegroup',
  191. AVG(VALUE_AVG.Value) as 'avg',
  192. MAX(VALUE_PEAK.Value) as 'peak'${
  193. select_max
  194. ? `,
  195. MAX(VALUE_MAX.Value) as 'max'`
  196. : ''
  197. }
  198. FROM ServerDataEntry
  199. JOIN ServerDataValue AS VALUE_AVG ON ServerDataEntry.ID = VALUE_AVG.EntryID AND VALUE_AVG.Type = ? AND VALUE_AVG.Key = 'avg'
  200. JOIN ServerDataValue AS VALUE_PEAK ON ServerDataEntry.ID = VALUE_PEAK.EntryID AND VALUE_PEAK.Type = ? AND VALUE_PEAK.Key = 'peak'
  201. ${
  202. select_max
  203. ? "JOIN ServerDataValue AS VALUE_MAX ON ServerDataEntry.ID = VALUE_MAX.EntryID AND VALUE_MAX.Type = ? AND VALUE_MAX.Key = 'max'"
  204. : ''
  205. }
  206. WHERE ServerDataEntry.ServerID = ?
  207. AND ServerDataEntry.Timestamp BETWEEN ? AND ?
  208. GROUP BY Timegroup
  209. ORDER BY Timegroup;
  210. `,
  211. [sectionMs, sectionMs, ...select_types, serverID, from.getTime(), to.getTime()]
  212. );
  213. return result.rows.map(r => ({ time: new Date(r.Timegroup), avg: r.avg, peak: r.peak, max: r.max }));
  214. }
  215. public async queryServerStats(serverID: number, type: ServerDataType, from: Date, to: Date): Promise<ReducedValuesPerc> {
  216. const select_max = type !== 'cpu';
  217. const select_types = select_max ? [type, type, type] : [type, type];
  218. const result = await this.stmt(
  219. `
  220. SELECT
  221. AVG(VALUE_AVG.Value) as 'avg',
  222. AVG(VALUE_PEAK.Value) as 'peak'${
  223. select_max
  224. ? `,
  225. MAX(VALUE_MAX.Value) as 'max'`
  226. : ''
  227. }
  228. FROM ServerDataEntry
  229. JOIN ServerDataValue AS VALUE_AVG ON ServerDataEntry.ID = VALUE_AVG.EntryID AND VALUE_AVG.Type = ? AND VALUE_AVG.Key = 'avg'
  230. JOIN ServerDataValue AS VALUE_PEAK ON ServerDataEntry.ID = VALUE_PEAK.EntryID AND VALUE_PEAK.Type = ? AND VALUE_PEAK.Key = 'peak'
  231. ${
  232. select_max
  233. ? "JOIN ServerDataValue AS VALUE_MAX ON ServerDataEntry.ID = VALUE_MAX.EntryID AND VALUE_MAX.Type = ? AND VALUE_MAX.Key = 'max'"
  234. : ''
  235. }
  236. WHERE ServerDataEntry.ServerID = ?
  237. AND ServerDataEntry.Timestamp BETWEEN ? AND ?;
  238. `,
  239. [...select_types, serverID, from.getTime(), to.getTime()]
  240. );
  241. const row = result.rows[0];
  242. if (Object.keys(row).includes('max')) {
  243. return {
  244. avg: ((row['avg'] as number) / (row['max'] as number)) * 100,
  245. peak: ((row['peak'] as number) / (row['max'] as number)) * 100
  246. };
  247. } else {
  248. return {
  249. avg: row['avg'] as number,
  250. peak: row['peak'] as number
  251. };
  252. }
  253. }
  254. private async getHealthCheckConfigs(serverID?: number, type = 'http') {
  255. const res = await this.stmt(
  256. `SELECT
  257. HealthCheckConfig.*,
  258. HealthCheckParams.Type as '_ParamType',
  259. HealthCheckParams.Key as '_ParamKey',
  260. HealthCheckParams.Value as '_ParamValue'
  261. FROM HealthCheckConfig
  262. LEFT OUTER JOIN HealthCheckParams ON HealthCheckConfig.ID = HealthCheckParams.ConfigID
  263. WHERE HealthCheckConfig.Type = ?
  264. ${!!serverID ? 'AND HealthCheckConfig.ServerID = ?' : ''}
  265. ORDER BY HealthCheckConfig.Title, _ParamType, _ParamKey`,
  266. [type, serverID]
  267. );
  268. return this.configFromResultRows(res.rows);
  269. }
  270. public async getHttpCheckConfigs(serverID?: number) {
  271. return (await this.getHealthCheckConfigs(serverID)).map(this.httpCheckConfigFrom);
  272. }
  273. private async getHealthCheckConfigByID(serverID: number, configID: number) {
  274. if (!serverID && !configID) return null;
  275. const res = await this.stmt(
  276. `SELECT
  277. HealthCheckConfig.*,
  278. HealthCheckParams.Type as '_ParamType',
  279. HealthCheckParams.Key as '_ParamKey',
  280. HealthCheckParams.Value as '_ParamValue'
  281. FROM HealthCheckConfig
  282. LEFT OUTER JOIN HealthCheckParams ON HealthCheckConfig.ID = HealthCheckParams.ConfigID
  283. WHERE HealthCheckConfig.ID = ?
  284. AND HealthCheckConfig.ServerID = ?
  285. ORDER BY HealthCheckConfig.Title, _ParamType, _ParamKey`,
  286. [configID, serverID]
  287. );
  288. if (!res.rows.length) return null;
  289. const configs = this.configFromResultRows(res.rows);
  290. return configs[0];
  291. }
  292. public async getHttpCheckConfigByID(serverID: number, configID: number) {
  293. return this.httpCheckConfigFrom(await this.getHealthCheckConfigByID(serverID, configID));
  294. }
  295. public async saveHttpCheckConfig(serverID: number, conf: HttpCheckConfig) {
  296. const validationErrors = this.validateHttpCheckConfig(conf);
  297. if (validationErrors) throw new ValidationException('Validation of HttpCheckConfig object failed', validationErrors);
  298. conf.serverId = serverID;
  299. let status = ServiceChangedStatus.None;
  300. const oldConf = await this.getHttpCheckConfigByID(serverID, conf.id);
  301. await this.beginTransaction();
  302. try {
  303. if (oldConf) {
  304. // UPDATE
  305. Logger.debug('[DEBUG] Updating HealthCheckConfig', conf.title, `(${oldConf.id})`);
  306. if (oldConf.title !== conf.title) {
  307. await this.stmt('UPDATE HealthCheckConfig SET Title = ? WHERE ID = ?', [conf.title, oldConf.id]);
  308. }
  309. let updValues: any[][] = [];
  310. if (oldConf.url !== conf.url) updValues.push([conf.url, conf.id, 'url']);
  311. if (oldConf.interval !== conf.interval) {
  312. updValues.push([conf.interval, conf.id, 'interval']);
  313. status = ServiceChangedStatus.Rescheduled;
  314. }
  315. if (oldConf.timeout !== conf.timeout) updValues.push([conf.timeout ?? defaults.serviceChecks.httpTimeout, conf.id, 'timeout']);
  316. if (oldConf.active !== conf.active) {
  317. updValues.push([conf.active ?? defaults.serviceChecks.active ? 1 : 0, conf.id, 'active']);
  318. status = conf.active ?? defaults.serviceChecks.active ? ServiceChangedStatus.Activated : ServiceChangedStatus.Deactivated;
  319. }
  320. if (oldConf.notify !== conf.notify) updValues.push([conf.notify ?? defaults.serviceChecks.notify ? 1 : 0, conf.id, 'notify']);
  321. if (oldConf.notifyThreshold !== conf.notifyThreshold)
  322. updValues.push([conf.notifyThreshold ?? defaults.serviceChecks.notifyThreshold, conf.id, 'notifyThreshold']);
  323. if (updValues.length) {
  324. for (const data of updValues) {
  325. await this.run(`UPDATE HealthCheckParams SET Value = ? WHERE ConfigID = ? AND Key = ?;`, data);
  326. }
  327. }
  328. const res = await this.stmt('SELECT * FROM HealthCheckParams WHERE ConfigID = ? and Key = "check";', [conf.id]);
  329. updValues = [];
  330. const delIDs: number[] = [];
  331. res.rows.forEach((row, i) => {
  332. if (i < conf.checks.length) {
  333. updValues.push([JSON.stringify(conf.checks[i]), row['ID']]);
  334. } else {
  335. delIDs.push(row['ID']);
  336. }
  337. });
  338. if (delIDs.length) {
  339. const delSql = `DELETE FROM HealthCheckParams WHERE ID IN (${delIDs.map(() => '?').join(',')});`;
  340. await this.run(delSql, delIDs);
  341. }
  342. if (updValues.length) {
  343. for (const data of updValues) {
  344. await this.run('UPDATE HealthCheckParams SET Value = ? WHERE ID = ?;', data);
  345. }
  346. }
  347. const insValues = conf.checks.filter((c, i) => i > res.rows.length - 1).map(c => [conf.id, 'regexp', 'check', JSON.stringify(c)]);
  348. if (insValues.length) {
  349. for (const data of insValues) {
  350. await this.run('INSERT INTO HealthCheckParams(ConfigID, Type, Key, Value) VALUES(?, ?, ?, ?);', data);
  351. }
  352. }
  353. } else {
  354. // INSERT
  355. Logger.debug('[DEBUG] Inserting new HealthCheckConfig', conf.title);
  356. const res = await this.run('INSERT INTO HealthCheckConfig(ServerID, Type, Title) VALUES(?, ?, ?);', [serverID, 'http', conf.title]);
  357. conf.id = res.lastID;
  358. if (conf.active ?? defaults.serviceChecks.active) {
  359. status = ServiceChangedStatus.Created;
  360. }
  361. const insCheckValues = conf.checks.map(c => [res.lastID, 'regexp', 'check', c]);
  362. await this.run(
  363. `INSERT INTO HealthCheckParams(ConfigID, Type, Key, Value) VALUES
  364. (?, ?, ?, ?),
  365. (?, ?, ?, ?),
  366. (?, ?, ?, ?),
  367. (?, ?, ?, ?),
  368. (?, ?, ?, ?),
  369. (?, ?, ?, ?)${conf.checks.length ? `,${insCheckValues.map(() => '(?, ?, ?, ?)').join(',')}` : ''}`,
  370. [
  371. ...[res.lastID, 'text', 'url', conf.url],
  372. ...[res.lastID, 'boolean', 'active', conf.active ?? defaults.serviceChecks.active ? 1 : 0],
  373. ...[res.lastID, 'number', 'interval', conf.interval],
  374. ...[res.lastID, 'number', 'timeout', conf.timeout ?? defaults.serviceChecks.httpTimeout],
  375. ...[res.lastID, 'boolean', 'notify', conf.notify ?? defaults.serviceChecks.notify],
  376. ...[res.lastID, 'number', 'notifyThreshold', conf.notifyThreshold ?? defaults.serviceChecks.notifyThreshold],
  377. ...conf.checks.reduce((ret, check) => [...ret, res.lastID, 'regexp', 'check', JSON.stringify(check)], [] as any[])
  378. ]
  379. );
  380. }
  381. await this.commit();
  382. return { status, result: conf };
  383. } catch (err) {
  384. await this.rollback();
  385. throw err;
  386. }
  387. }
  388. async deleteHealthCheckConfig(serverID: number, confID: number) {
  389. const conf = await this.getHealthCheckConfigByID(serverID, confID);
  390. if (!conf) return false;
  391. await this.run('DELETE FROM HealthCheckConfig WHERE ID = ?;', [confID]);
  392. return true;
  393. }
  394. async insertHealthCheckData(confID: number, time: Date, status: HttpCheckStatus, message: string) {
  395. const res = await this.run('INSERT INTO HealthCheckDataEntry(ConfigID, Timestamp, Status, Message) VALUES(?, ?, ?, ?);', [
  396. confID,
  397. time.getTime(),
  398. status,
  399. message
  400. ]);
  401. return {
  402. id: res.lastID,
  403. configId: confID,
  404. time,
  405. status,
  406. message
  407. } as HttpCheckData;
  408. }
  409. async queryServiceCheckData(serverID: number, confID: number, from: Date, to: Date) {
  410. const result = await this.stmt(
  411. `
  412. SELECT DataEntryChanges.*
  413. FROM HealthCheckConfig
  414. JOIN (
  415. SELECT * FROM (
  416. SELECT
  417. *
  418. FROM HealthCheckDataEntry
  419. WHERE ConfigID = ?
  420. AND Timestamp BETWEEN ? AND ?
  421. ORDER BY ID
  422. LIMIT 0, 1
  423. ) AS FIRST_STATE
  424. UNION --+--+--
  425. SELECT
  426. ID,
  427. ConfigID,
  428. Timestamp,
  429. Status,
  430. Message
  431. FROM
  432. (
  433. SELECT
  434. HealthCheckDataEntry.*,
  435. LAG(Status) OVER (ORDER BY ConfigID, Timestamp) AS previous_state,
  436. LAG(Message) OVER (ORDER BY ConfigID, Timestamp) AS previous_msg
  437. FROM HealthCheckDataEntry
  438. WHERE ConfigID = ?
  439. )
  440. WHERE Status <> previous_state
  441. AND Message <> previous_msg
  442. UNION --+--+--
  443. SELECT * FROM (
  444. SELECT
  445. *
  446. FROM HealthCheckDataEntry
  447. WHERE ConfigID = ?
  448. AND Timestamp BETWEEN ? AND ?
  449. ORDER BY ID DESC
  450. LIMIT 0, 1
  451. ) AS LAST_STATE
  452. ORDER BY ConfigID, Timestamp
  453. ) AS DataEntryChanges ON DataEntryChanges.ConfigID = HealthCheckConfig.ID
  454. WHERE HealthCheckConfig.ServerID = ?
  455. AND DataEntryChanges.Timestamp BETWEEN ? AND ?
  456. ORDER BY Timestamp, ID;`,
  457. [confID, from.getTime(), to.getTime(), confID, confID, from.getTime(), to.getTime(), serverID, from.getTime(), to.getTime()]
  458. );
  459. const mapByTimestamp = this.mapServiceCheckDataByTimestamp(result.rows);
  460. const arr: ServiceCheckData[] = [];
  461. for (const entry of mapByTimestamp.entries()) {
  462. arr.push({
  463. time: new Date(entry[0]),
  464. data: entry[1]
  465. });
  466. }
  467. return arr;
  468. }
  469. public async queryServiceCheckLogs(serverID: number, confID: number, from: Date, to: Date) {
  470. const result = await this.stmt(
  471. `
  472. SELECT HealthCheckDataEntry.*
  473. FROM HealthCheckConfig
  474. JOIN HealthCheckDataEntry ON HealthCheckDataEntry.ConfigID = HealthCheckConfig.ID
  475. WHERE HealthCheckConfig.ID = ?
  476. AND HealthCheckConfig.ServerID = ?
  477. AND HealthCheckDataEntry.Timestamp BETWEEN ? AND ?
  478. ORDER BY Timestamp, ConfigID;`,
  479. [confID, serverID, from.getTime(), to.getTime()]
  480. );
  481. const mapByTimestamp = this.mapServiceCheckDataByTimestamp(result.rows);
  482. const arr: ServiceCheckData[] = [];
  483. for (const entry of mapByTimestamp.entries()) {
  484. arr.push({
  485. time: new Date(entry[0]),
  486. data: entry[1]
  487. });
  488. }
  489. return arr;
  490. }
  491. public async getLastErrors(confID: number, threshold: number) {
  492. const result = await this.stmt(
  493. `SELECT * FROM HealthCheckDataEntry
  494. WHERE ConfigID = ?
  495. AND Timestamp IN (
  496. SELECT Timestamp
  497. FROM HealthCheckDataEntry
  498. WHERE ConfigID = ?
  499. GROUP BY Timestamp
  500. ORDER BY Timestamp DESC
  501. LIMIT 0, ?
  502. )
  503. ORDER BY Timestamp DESC, ID DESC`,
  504. [confID, confID, threshold]
  505. );
  506. const mapByTimestamp = this.mapServiceCheckDataByTimestamp(result.rows);
  507. const errors: ServiceCheckData[] = [];
  508. for (const entry of mapByTimestamp.entries()) {
  509. const time = entry[0];
  510. const data = entry[1];
  511. const errorData = data.filter(d => d.status !== HttpCheckStatus.OK);
  512. if (!errorData.length) break;
  513. errors.push({
  514. time: new Date(time),
  515. data: errorData
  516. });
  517. }
  518. return errors;
  519. }
  520. private mapServiceCheckDataByTimestamp(rows: any[]) {
  521. return rows.reduce((res: Map<number, ServiceCheckDataEntry[]>, row) => {
  522. const time: number = row['Timestamp'];
  523. if (!res.has(time)) res.set(time, []);
  524. res.get(time)?.push({
  525. status: row['Status'] as number,
  526. message: row['Message']
  527. });
  528. return res;
  529. }, new Map()) as Map<number, ServiceCheckDataEntry[]>;
  530. }
  531. private configFromResultRows(rows: any[]) {
  532. return rows.reduce((res: ServiceConfig[], line, i) => {
  533. const configID = line['ID'];
  534. let config: ServiceConfig;
  535. if (i === 0 || res[res.length - 1].id !== configID) {
  536. config = {
  537. id: configID,
  538. title: line['Title'],
  539. type: line['Type'],
  540. serverId: line['ServerID'],
  541. params: []
  542. };
  543. res.push(config);
  544. } else {
  545. config = res[res.length - 1];
  546. }
  547. if (!!line['_ParamKey']) {
  548. const type = validateParamType(line['_ParamType']);
  549. const key = line['_ParamKey'];
  550. if (key === 'check') {
  551. let checkParam = config.params.find(c => c.key === 'check');
  552. if (!checkParam) {
  553. config.params.push(
  554. (checkParam = {
  555. key: 'check',
  556. type: 'regexp',
  557. value: []
  558. })
  559. );
  560. }
  561. (checkParam.value as string[]).push(line['_ParamValue']);
  562. } else {
  563. config.params.push({
  564. type,
  565. key,
  566. value: type === 'number' ? Number(line['_ParamValue']) : type === 'boolean' ? Boolean(Number(line['_ParamValue'])) : line['_ParamValue']
  567. });
  568. }
  569. }
  570. return res;
  571. }, [] as ServiceConfig[]);
  572. }
  573. private httpCheckConfigFrom(hcConf: ServiceConfig | null): HttpCheckConfig | null {
  574. if (!hcConf) return null;
  575. const params = {
  576. url: hcConf.params?.find(p => p.key === 'url')?.value as string,
  577. active: (hcConf.params?.find(p => p.key === 'active')?.value as boolean) ?? defaults.serviceChecks.active,
  578. interval: hcConf.params?.find(p => p.key === 'interval')?.value as number,
  579. timeout: (hcConf.params?.find(p => p.key === 'timeout')?.value as number) ?? defaults.serviceChecks.httpTimeout,
  580. notify: (hcConf.params?.find(p => p.key === 'notify')?.value as boolean) ?? defaults.serviceChecks.notify,
  581. notifyThreshold: (hcConf.params?.find(p => p.key === 'notifyThreshold')?.value as number) ?? defaults.serviceChecks.notifyThreshold,
  582. checks: hcConf.params?.reduce(
  583. (res, p) => (p.key === 'check' && Array.isArray(p.value) ? [...res, ...p.value.map(c => JSON.parse(c))] : res),
  584. [] as string[]
  585. )
  586. };
  587. return {
  588. id: hcConf.id,
  589. title: hcConf.title,
  590. type: hcConf.type,
  591. serverId: hcConf.serverId,
  592. ...params
  593. };
  594. }
  595. private validateHttpCheckConfig(conf: Partial<HttpCheckConfig>): { [key: string]: string } | null {
  596. const errors = {} as any;
  597. if (!conf) return { null: 'Object was null or undefined' };
  598. if (!conf.title?.trim().length) errors['required|title'] = `Field 'title' is required.`;
  599. if (!conf.url?.trim().length) errors['required|url'] = `Field 'url' is required.`;
  600. if ((!conf.interval && conf.interval !== 0) || Number.isNaN(Number(conf.interval))) errors['required|interval'] = `Field 'interval' is required.`;
  601. if (!conf.checks || !Array.isArray(conf.checks))
  602. errors['required|checks'] = `Field 'checks' is required and must be an array of check expressions.`;
  603. return Object.keys(errors).length ? errors : null;
  604. }
  605. }