Initial commit

Signed-off-by: Romein van Buren <romein@vburen.nl>
This commit is contained in:
2022-06-22 17:22:19 +02:00
commit 1983f78fba
17 changed files with 3016 additions and 0 deletions

25
lib/operators.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
const operatorNames = {
equal: 'equal to',
start: 'starting with',
end: 'ending in',
contain: 'containing',
greater: 'greater than',
less: 'less than',
greatereq: 'greater than or equal to',
lesseq: 'less than or equal to',
};
const operators = {
equal: (a, b) => String(a) === String(b),
start: (a, b) => String(a).startsWith(b),
end: (a, b) => String(a).startsWith(b),
contain: (a, b) => String(a).includes(b),
greater: (a, b) => Number(a) > Number(b),
less: (a, b) => Number(a) < Number(b),
greatereq: (a, b) => Number(a) >= Number(b),
lesseq: (a, b) => Number(a) <= Number(b),
};
module.exports = { operatorNames, operators };

21
lib/realvalues.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
const realValueNames = {
httpstatus: 'HTTP status code',
body: 'Response body',
bodylength: 'Response body length',
ok: 'Response OK?',
redir: 'Redirected?',
restype: 'Response type',
};
const realValues = {
httpstatus: ({ res }) => res.status,
body: ({ body }) => body,
bodylength: ({ body }) => body.length,
ok: ({ res }) => res.ok,
redir: ({ res }) => res.redirected,
restype: ({ res }) => res.type,
};
module.exports = { realValueNames, realValues };

66
lib/testendpoints.js Normal file
View File

@ -0,0 +1,66 @@
'use strict';
const fetch = require('node-fetch');
const { operators } = require('./operators');
const { realValues } = require('./realvalues');
async function testEndpoints(endpoints) {
const output = {
serviceUp: true,
success: true,
error: false,
requirement: undefined,
realValue: undefined,
};
for (const endpoint of endpoints) {
try {
const headers = endpoint.headers.reduce((obj, item) => {
obj[item.name] = item.value;
return obj;
}, {});
const res = await fetch(endpoint.uri, { headers });
const body = await res.text();
endpoint.requirements.forEach(requirement => {
if (output.success === false || output.serviceUp === false) {
return;
}
if (!Object.keys(operators).includes(requirement.operator)) {
output.success = false;
output.error = 'unknown operator: ' + requirement.operator;
}
if (!Object.keys(realValues).includes(requirement.type)) {
output.success = false;
output.error = 'unknown type: ' + requirement.type;
}
const realValue = realValues[requirement.type]({ res, body });
let result = operators[requirement.operator](realValue, requirement.string);
if (!requirement.truth) {
result = !result;
}
if (!result) {
output.success = true;
output.serviceUp = false;
output.requirement = requirement;
output.realValue = realValue;
}
});
}
catch (err) {
output.success = false;
output.error = err;
console.error(err);
}
}
return output;
}
module.exports = { testEndpoints };

138
lib/testservice.js Normal file
View File

@ -0,0 +1,138 @@
'use strict';
const { testEndpoints } = require('./testendpoints');
const { roundDate } = require('./utils');
async function testService({ service, server, settings, makeId }) {
if (!service.autotestEnabled) {
return;
}
try {
// Autotest the service
const result = await testEndpoints(service.autotest);
const name = service.name.en;
// Insert check date
await server.storage.store('smartyellow/webservice').update(
{ id: service.id },
{ $set: { lastChecked: new Date() } }
);
// Get all heartbeats plus the last one
const heartbeat = await server
.storage
.store('smartyellow/webserviceheartbeat')
.find({ webservice: service.id })
.toArray();
const lastBeat = heartbeat[heartbeat.length - 1];
// Get date
const date = roundDate(new Date());
// Error
if (result.error) {
server.error('Error while checking status: ' + name);
server.error(result);
}
// Service down
else if (!result.serviceUp) {
server.warn('Service down: ' + name);
server.warn(result);
// Don't perform automatic actions if already done
if ((lastBeat && lastBeat.down == false) || !lastBeat) {
// Insert heartbeat if last one is not valid anymore
try {
await server.storage.store('smartyellow/webserviceheartbeat').insert({
id: makeId(6),
down: true,
webservice: service.id,
testResult: result,
date: date,
});
}
catch (err) {
server.error(err);
server.error('could not save web service heartbeat');
}
// Send e-mail notification
if (server.sendEmail && settings.emailSender && settings.emailRecipient) {
try {
await server.sendEmail({
sender: settings.emailSender,
to: settings.emailRecipient,
subject: `[outage] ${name} is down`,
body: `<p>Dear recipient,</p>
<p>This is to inform you about web service outage.
The service <em>${name}</em> does not meet the
requirements for being considered as 'working'.</p>
<p>Please always check this before taking action.</p>`,
});
}
catch (err) {
server.error(err);
server.error('could not send endpoint status notification e-mail');
}
}
// Draft outage entry
if (settings.draftOutageEntries) {
try {
await server
.storage
.store('smartyellow/webserviceoutage')
.insert({
id: makeId(6),
name: {
en: `[automatic] Outage for ${name}`,
},
state: 'concept',
resolved: false,
services: [ service.id ],
tags: [ 'automatically created' ],
notes: [ {
date: new Date(),
userId: 'system',
text: `Automatically created outage. Reason: ${JSON.stringify(result, null, 2)}`,
} ],
});
}
catch (err) {
server.error(err);
server.error('could not automatically draft outage entry');
}
}
}
}
// Service up
else {
server.info('Service up: ' + name);
// Insert heartbeat if last one is not valid anymore
if ((lastBeat && lastBeat.down == true) || !lastBeat) {
try {
await server.storage.store('smartyellow/webserviceheartbeat').insert({
id: makeId(6),
down: false,
webservice: service.id,
date: date,
testResult: result,
});
}
catch (err) {
server.error(err);
server.error('could not save web service heartbeat');
}
}
}
}
catch (err) {
server.error(err);
}
}
module.exports = { testService };

19
lib/testservices.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
const { testService } = require('./testservice');
async function testServices({ server, settings, makeId }) {
const services = await server
.storage
.store('smartyellow/webservice')
.find({ autotestEnabled: true })
.toArray();
services.forEach(async service => {
if (service.autotestEnabled) {
testService({ service, server, settings, makeId });
}
});
}
module.exports = { testServices };

11
lib/utils.js Normal file
View File

@ -0,0 +1,11 @@
'use strict';
// Round minutes up to 10 and remove seconds
// e.g. 15:34:51 -> 15:30:00
function roundDate(d) {
d.setMinutes(Math.round(d.getMinutes() / 10) * 10);
d.setSeconds(0, 0);
return d;
}
module.exports = { roundDate };