'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); } }, }, ], };