'use strict';
const { fork } = require('child_process');
const { processOutage } = require('./lib/processoutage');
const buildDashboard = require('./lib/dashboard/build');
const createDashboardSocket = require('./lib/dashboard/socket');
const { minifyHtml } = require('core/strings');
const { readFile } = require('fs/promises');
const guiCluster = 'web service status';
const icons = {
server: '',
outage: '',
checks: '',
};
let renderedDashboard = null;
module.exports = {
// Friendly name
name: 'Status',
// Brief description of this plugin
purpose: 'Keep track of the status of your web services automatically and get notified when a service is down.',
// Version of this plugin
version: '1.0.0',
// Name of the plugin author
author: 'Romein van Buren',
// Name of vendor of this plugin
vendor: 'Smart Yellow',
// Array of plugins this plugin depends on
requires: [ 'webdesq/storage' ],
// Features this plugin offers
features: {
seeServices: {
description: 'See all web services',
},
createServices: {
description: 'Create new web services',
requires: [ 'seeServices' ],
},
editServices: {
description: 'Edit web services',
requires: [ 'seeServices' ],
},
deleteServices: {
description: 'Delete web services',
requires: [ 'editServices' ],
},
seeOutages: {
description: 'See all outages',
requires: [ 'seeServices' ],
},
createOutages: {
description: 'Create new outages',
requires: [ 'seeServices', 'seeOutages' ],
},
editOutages: {
description: 'Edit outages',
requires: [ 'seeServices', 'seeOutages' ],
},
deleteOutages: {
description: 'Delete outages',
requires: [ 'seeServices', 'editOutages' ],
},
seeMonitor: {
description: 'See the monitor',
},
},
icon: icons.server,
entities: {
webservice: 'webservice.js',
webserviceoutage: 'webserviceoutage.js',
webserviceheartbeat: 'webserviceheartbeat.js',
},
settings: {
clusters: {
type: 'keys',
label: 'clusters',
description: 'Clusters can be used to catogorise web services into groups.',
default: {},
},
serviceTags: {
type: 'keys',
label: 'service tags',
description: 'Tags that can be assigned to web services to categorise them.',
default: {},
},
outageTags: {
type: 'keys',
label: 'outage tags',
description: 'Tags that can be assigned to outage messages to categorise them.',
default: {},
},
emailSender: {
type: 'string',
label: 'notification sender',
description: 'Sender of notifications about service statuses. Format: Name ',
default: '',
},
emailRecipient: {
type: 'array',
label: 'notification recipients',
description: 'Recipients of notifications about service statuses. Format: Name ',
default: [],
},
draftOutageEntries: {
type: 'boolean',
label: 'draft outage entries',
description: 'Automatically draft an outage entry when a service is down?',
default: true,
},
},
gui: {
components: [
'formautotestfield.svelte',
'formoutagetablefield.svelte',
],
modules: () => [
{ path: 'webservices.svelte',
requires: [ 'seeServices', 'editServices' ],
menu: {
cluster: guiCluster,
icon: icons.server,
title: 'web services',
},
},
{ path: 'webserviceoutages.svelte',
requires: [ 'seeServices', 'editServices' ],
menu: {
cluster: guiCluster,
icon: icons.outage,
title: 'outages',
},
},
{ path: 'webservicemonitor.svelte',
requires: [ 'seeMonitor' ],
menu: {
cluster: guiCluster,
icon: icons.checks,
title: 'monitor',
},
},
],
widgets: () => [
{ path: 'webservicestatus.svelte',
title: 'Web service status',
purpose: 'Monitor web service status',
defaults: {
title: 'Web service status',
},
},
],
},
jobs: ({ server, settings }) => [
{ id: 'autotest',
purpose: 'Check whether services are up and send a notification if not.',
mandatory: false,
runAtBoot: true,
active: true,
interval: 60 * 1000,
action: async () => {
const services = await server
.storage
.store('smartyellow/webservice')
.find({ autotestEnabled: true })
.toArray();
if (!services.length) {
return;
}
const runtime = fork(__dirname + '/lib/runtime.js');
runtime.send({ command: 'testAll', services });
runtime.on('message', message => {
if (message.error) {
server.error(message.error);
}
else if (message.outage) {
processOutage({ outage: message.outage, server, settings });
}
});
},
},
],
hooks: ({ server, settings }) => [
{ id: 'startDashboardSocket',
event: 'boot',
order: 100,
purpose: 'Start the websocket for the dashboard after boot',
handler: () => createDashboardSocket(server),
},
{ id: 'autotestOnSave',
order: 500,
event: 'saveEntity',
entity: [ 'smartyellow/webservice' ],
purpose: 'Check whether services are up and send a notification if not.',
handler: ({ item }) => {
const runtime = fork(__dirname + '/lib/runtime.js');
runtime.send({ command: 'testOne', service: item });
runtime.on('message', message => {
if (message.error) {
server.error(message.error);
}
else if (message.outage) {
processOutage({
outage: {
[item.id]: message.outage,
},
server,
settings,
});
}
});
},
},
],
routes: ({ server, settings }) => [
// Get all services
{ route: '/webservices',
method: 'get',
requires: 'smartyellow/status/seeServices',
handler: async (req, res, user) => {
const services = server.storage({ user }).store('smartyellow/webservice').find();
const result = await (req.headers['format'] == 'object' ? services.toObject() : services.toArray());
if (req.headers['format'] == 'object') {
for (const service of Object.keys(result)) {
result[service].heartbeat = await server
.storage
.store('smartyellow/webserviceheartbeat')
.find({ service: service.id })
.toArray();
}
}
else {
for (const [ i, service ] of result.entries()) {
result[i].heartbeat = await server
.storage
.store('smartyellow/webserviceheartbeat')
.find({ webservice: service.id })
.toArray();
}
}
res.json(result);
},
},
// Get details for specific service
{ route: '/webservices/:id',
method: 'get',
requires: 'smartyellow/status/seeServices',
handler: async (req, res, user) => {
const doc = await server.storage({ user }).store('smartyellow/webservice').get(req.params[0]);
const result = await server.validateEntity({
entity: 'smartyellow/webservice',
id: req.params[0],
data: doc,
validateOnly: true,
user: user,
isNew: false,
});
res.json(result);
},
},
{ route: '/webservices/search',
method: 'post',
requires: 'smartyellow/status/seeServices',
handler: async (req, res, user) => {
const filters = await server.getFilters({
entity: 'smartyellow/webservice',
user: user,
});
const q = server.storage({ user }).prepareQuery(filters, req.body.query, req.body.languages || false);
const result = await server.storage({ user }).store('smartyellow/webservice').find(q).sort({ 'log.created.on': -1 }).toArray();
res.json(result);
},
},
// Get filters for services
{ route: '/webservices/filters',
method: 'get',
requires: 'smartyellow/status/seeServices',
handler: async (req, res, user) => {
res.json(await server.getFilters({
entity: 'smartyellow/webservice',
user: user,
}));
},
},
// Get formats for services
{ route: '/webservices/formats',
method: 'get',
requires: 'smartyellow/status/seeServices',
handler: async (req, res, user) => {
const formats = await server.getFormats({
entity: 'smartyellow/webservice',
user: user,
});
res.json(formats);
},
},
// Create new service
{ route: '/webservices',
method: 'post',
requires: 'smartyellow/status/createServices',
handler: async (req, res, user) => {
// Validate the posted data
const result = await server.validateEntity({
entity: 'smartyellow/webservice',
data: req.body,
storeIfValid: true,
validateOnly: req.headers['init'],
form: req.headers['form'] || 'default',
isNew: true,
user: user,
});
if (!result.errors) {
// broadcast message to all clients to notify users have been changed
server.publish('cms', 'smartyellow/status/reload');
}
return res.json(result);
},
},
// Update existing service
{ route: '/webservices/:id',
method: 'put',
requires: 'smartyellow/status/editServices',
handler: async (req, res, user) => {
// Validate the posted data
const result = await server.validateEntity({
entity: 'smartyellow/webservice',
id: req.params[0],
data: req.body,
storeIfValid: true,
validateOnly: req.headers['init'],
form: req.headers['form'] || 'default',
isNew: false,
user: user,
});
if (!result.errors) {
// broadcast message to all clients to notify users have been changed
server.publish('cms', 'smartyellow/status/reload');
}
return res.json(result);
},
},
// Delete specific service
{ route: '/webservices/:id',
method: 'delete',
requires: 'smartyellow/status/deleteServices',
handler: async (req, res, user) => {
const result = await server.storage({ user }).store('smartyellow/webservice').delete({ id: req.params[0] });
if (!result.errors) {
// broadcast message to all clients to notify users have been changed
server.publish('cms', 'smartyellow/status/reload');
}
res.json(result);
},
},
// Get all outages
{ route: '/outages',
method: 'get',
requires: 'smartyellow/status/seeOutages',
handler: async (req, res, user) => {
const outages = server.storage({ user }).store('smartyellow/webserviceoutage').find();
const result = await (req.headers['format'] == 'object' ? outages.toObject() : outages.toArray());
res.json(result);
},
},
// Get details for specific outage
{ route: '/outages/:id',
method: 'get',
requires: 'smartyellow/status/seeOutages',
handler: async (req, res, user) => {
const doc = await server.storage({ user }).store('smartyellow/webserviceoutage').get(req.params[0]);
const result = await server.validateEntity({
entity: 'smartyellow/webserviceoutage',
id: req.params[0],
data: doc,
validateOnly: true,
user: user,
isNew: false,
});
res.json(result);
},
},
{ route: '/outages/search',
method: 'post',
requires: 'smartyellow/status/seeOutages',
handler: async (req, res, user) => {
const filters = await server.getFilters({
entity: 'smartyellow/webserviceoutage',
user: user,
});
const q = server.storage({ user }).prepareQuery(filters, req.body.query, req.body.languages || false);
const result = await server.storage({ user }).store('smartyellow/webserviceoutage').find(q).sort({ 'log.created.on': -1 }).toArray();
res.json(result);
},
},
// Get filters for outages
{ route: '/outages/filters',
method: 'get',
requires: 'smartyellow/status/seeOutages',
handler: async (req, res, user) => {
res.json(await server.getFilters({
entity: 'smartyellow/webserviceoutage',
user: user,
}));
},
},
// Get formats for outages
{ route: '/outages/formats',
method: 'get',
requires: 'smartyellow/status/seeOutages',
handler: async (req, res, user) => {
const formats = await server.getFormats({
entity: 'smartyellow/webserviceoutage',
user: user,
});
res.json(formats);
},
},
// Create new service
{ route: '/outages',
method: 'post',
requires: 'smartyellow/status/createOutages',
handler: async (req, res, user) => {
// Validate the posted data
const result = await server.validateEntity({
entity: 'smartyellow/webserviceoutage',
data: req.body,
storeIfValid: true,
validateOnly: req.headers['init'],
form: req.headers['form'] || 'default',
isNew: true,
user: user,
});
if (!result.errors) {
// broadcast message to all clients to notify users have been changed
server.publish('cms', 'smartyellow/status/reload');
}
return res.json(result);
},
},
// Update existing service
{ route: '/outages/:id',
method: 'put',
requires: 'smartyellow/status/editOutages',
handler: async (req, res, user) => {
// Validate the posted data
const result = await server.validateEntity({
entity: 'smartyellow/webserviceoutage',
id: req.params[0],
data: req.body,
storeIfValid: true,
validateOnly: req.headers['init'],
form: req.headers['form'] || 'default',
isNew: false,
user: user,
});
if (!result.errors) {
// broadcast message to all clients to notify users have been changed
server.publish('cms', 'smartyellow/status/reload');
}
return res.json(result);
},
},
// Delete specific service
{ route: '/outages/:id',
method: 'delete',
requires: 'smartyellow/status/deleteOutages',
handler: async (req, res, user) => {
const result = await server.storage({ user }).store('smartyellow/webserviceoutage').delete({ id: req.params[0] });
if (!result.errors) {
// broadcast message to all clients to notify users have been changed
server.publish('cms', 'smartyellow/status/reload');
}
res.json(result);
},
},
{ route: '/statusdashboard',
method: 'get',
handler: async (req, res) => {
try {
if (!renderedDashboard) {
renderedDashboard = await buildDashboard({ server, settings });
renderedDashboard.globalCss = await readFile(
__dirname + '/gui/dashboard/app.css'
);
}
const dashboardHtml = minifyHtml(`
Web service status dashboard
`);
res.send(dashboardHtml);
}
catch (error) {
server.error('could not compile web service status dashboard', error);
}
},
},
],
};