mirror of
https://github.com/smartyellow/status.git
synced 2025-01-18 05:27:58 +00:00
Merged some files
This commit is contained in:
parent
a2a11c3bd2
commit
bd9444f30f
@ -2,7 +2,7 @@ import { get, writable } from 'svelte/store';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { crossfade } from 'svelte/transition';
|
||||
|
||||
function createSettingsStore() {
|
||||
export const settings = (() => {
|
||||
const defaults = {
|
||||
theme: 'dark',
|
||||
cols: 4,
|
||||
@ -14,10 +14,7 @@ function createSettingsStore() {
|
||||
const s = writable(defaults);
|
||||
|
||||
function updateStorage(val) {
|
||||
window.localStorage.setItem('statusdash', JSON.stringify({
|
||||
...defaults,
|
||||
...val,
|
||||
}));
|
||||
window.localStorage.setItem('statusdash', JSON.stringify({ ...defaults, ...val }));
|
||||
s.set(val);
|
||||
}
|
||||
|
||||
@ -25,7 +22,7 @@ function createSettingsStore() {
|
||||
let localStorage = {};
|
||||
|
||||
try {
|
||||
localStorage = JSON.parse(localStorageString);
|
||||
localStorage = JSON.parse(localStorageString || '{}');
|
||||
}
|
||||
catch {
|
||||
localStorage = {};
|
||||
@ -35,12 +32,10 @@ function createSettingsStore() {
|
||||
|
||||
return {
|
||||
subscribe: s.subscribe,
|
||||
set: val => updateStorage(val),
|
||||
set: val => updateStorage(val || {}),
|
||||
update: val => updateStorage({ ...get(s), ...val }),
|
||||
};
|
||||
}
|
||||
|
||||
export const settings = createSettingsStore();
|
||||
})();
|
||||
|
||||
export const shuffle = crossfade({
|
||||
fallback(node) {
|
||||
|
319
index.js
319
index.js
@ -1,13 +1,18 @@
|
||||
'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 { makeId } = require('core/makeid');
|
||||
const buildDeps = {
|
||||
rollup: require('rollup').rollup,
|
||||
commonjs: require('@rollup/plugin-commonjs'),
|
||||
css: require('rollup-plugin-css-only'),
|
||||
resolve: require('@rollup/plugin-node-resolve').nodeResolve,
|
||||
svelte: require('rollup-plugin-svelte'),
|
||||
terser: require('rollup-plugin-terser').terser,
|
||||
};
|
||||
|
||||
const guiCluster = 'web service status';
|
||||
const icons = {
|
||||
server: '<path d="M0 308.58v150.83a57.73 57.73 0 0 0 10.69 33.38H757.3a57.62 57.62 0 0 0 10.7-33.37V308.58a57.73 57.73 0 0 0-10.69-33.38H10.7A57.76 57.76 0 0 0 0 308.58Zm665.6 81.82a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM640 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 0-25.6ZM588.8 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM537.6 352a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM512 390.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM486.4 352a12.8 12.8 0 1 1-.01 25.61 12.8 12.8 0 0 1 0-25.61Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6ZM435.2 352a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.61Zm-300.8-25.6A57.67 57.67 0 0 1 192 384a57.67 57.67 0 0 1-57.6 57.6A57.67 57.67 0 0 1 76.8 384a57.67 57.67 0 0 1 57.6-57.6Zm622.91-76.8A57.76 57.76 0 0 0 768 216.22V65.38A59.05 59.05 0 0 0 709.02 6.4H58.98A59.05 59.05 0 0 0 0 65.38V216.2a57.73 57.73 0 0 0 10.69 33.39H757.3Zm-91.7-102.4a12.8 12.8 0 1 1-.02 25.6 12.8 12.8 0 0 1 .01-25.6ZM640 108.8a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1-.01 25.6 12.8 12.8 0 0 1 .01-25.6ZM512 147.2a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1-.01 25.61 12.8 12.8 0 0 1 0-25.6Zm-25.6 38.4a12.8 12.8 0 1 1 0 25.6 12.8 12.8 0 0 1 0-25.6Zm-25.6-38.4a12.8 12.8 0 1 1 0 25.61 12.8 12.8 0 0 1 0-25.6ZM134.4 83.2a57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6 57.67 57.67 0 0 1-57.6-57.6 57.67 57.67 0 0 1 57.6-57.6ZM10.69 518.4A57.76 57.76 0 0 0 0 551.78v150.83c0 32.53 26.46 59 58.98 59H709a59.05 59.05 0 0 0 58.99-59V551.79a57.73 57.73 0 0 0-10.69-33.38Zm123.7 166.4a57.67 57.67 0 0 1-57.59-57.6 57.67 57.67 0 0 1 57.6-57.6 57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6Zm300.8-64a12.8 12.8 0 1 1 .02-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.61 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.6 12.8 12.8 0 0 1 0 25.6Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.61 12.8 12.8 0 0 1 0 25.6Zm0 0"/>',
|
||||
outage: '<path d="M601.6 435.2c-91.75 0-166.4 74.65-166.4 166.4S509.85 768 601.6 768 768 693.35 768 601.6s-74.65-166.4-166.4-166.4Zm0 307.2c-77.63 0-140.8-63.17-140.8-140.8 0-77.63 63.17-140.8 140.8-140.8 77.63 0 140.8 63.17 140.8 140.8 0 77.63-63.17 140.8-140.8 140.8Zm0 0"/><path d="M664.96 538.25c-5-5-13.1-5-18.1 0L601.6 583.5l-45.26-45.25a12.79 12.79 0 1 0-18.1 18.1l45.26 45.25-45.26 45.25a12.79 12.79 0 0 0 9.05 21.85 12.8 12.8 0 0 0 9.05-3.75l45.26-45.25 45.26 45.25a12.77 12.77 0 0 0 18.1 0c5-5 5-13.1 0-18.1L619.7 601.6l45.26-45.25c5-5 5-13.1 0-18.1ZM709.02 0H58.98A59.05 59.05 0 0 0 0 58.98V209.8a57.76 57.76 0 0 0 10.69 33.4H757.3a57.68 57.68 0 0 0 10.7-33.38V58.98A59.05 59.05 0 0 0 709.02 0ZM134.4 192a57.67 57.67 0 0 1-57.6-57.6 57.67 57.67 0 0 1 57.6-57.6 57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6Zm300.8-64a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.61Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 .01-25.61 12.8 12.8 0 0 1 0 25.61Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm25.6-38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.61Zm25.6 38.4a12.8 12.8 0 1 1 .01-25.6 12.8 12.8 0 0 1-.01 25.6ZM640 128a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.61Zm25.6 38.4a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6ZM396.8 601.6c0-32.15 7.65-62.5 20.93-89.6H10.69A57.76 57.76 0 0 0 0 545.38V696.2c0 32.54 26.46 59 58.98 59h407.6c-42.67-37.57-69.78-92.41-69.78-153.6Zm-262.4 76.8a57.67 57.67 0 0 1-57.6-57.6 57.67 57.67 0 0 1 57.6-57.6 57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6ZM10.69 268.8A57.75 57.75 0 0 0 0 302.18V453a57.75 57.75 0 0 0 10.69 33.4h421.7c36.92-54.04 98.98-89.6 169.2-89.6 65.57 0 123.9 31.1 161.4 79.2a56.8 56.8 0 0 0 5-22.98V302.18a57.73 57.73 0 0 0-10.69-33.38Zm123.7 166.4a57.67 57.67 0 0 1-57.59-57.6 57.67 57.67 0 0 1 57.6-57.6 57.67 57.67 0 0 1 57.6 57.6 57.67 57.67 0 0 1-57.6 57.6Zm300.8-64a12.8 12.8 0 1 1 .02-25.61 12.8 12.8 0 0 1-.01 25.6Zm51.2 0a12.8 12.8 0 1 1 .02-25.6 12.8 12.8 0 0 1-.01 25.6Zm51.21 0a12.8 12.8 0 1 1 .01-25.61 12.8 12.8 0 0 1 0 25.6Zm51.2 0a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm51.2 0a12.8 12.8 0 1 1 0-25.61 12.8 12.8 0 0 1 0 25.6Zm0 0"/>',
|
||||
@ -16,6 +21,142 @@ const icons = {
|
||||
|
||||
let renderedDashboard = null;
|
||||
|
||||
async function processOutage({ outage, server, settings, onDateUpdated }) {
|
||||
if (typeof onDateUpdated !== 'function') {
|
||||
onDateUpdated = () => null;
|
||||
}
|
||||
|
||||
for (const [ id, testResult ] of Object.entries(outage)) {
|
||||
// Update check date
|
||||
server.storage.store('smartyellow/webservice').update(
|
||||
{ id },
|
||||
{ $set: { lastChecked: new Date() } }
|
||||
).then(() => onDateUpdated(id));
|
||||
|
||||
// Get service entry
|
||||
const service = await server
|
||||
.storage
|
||||
.store('smartyellow/webservice')
|
||||
.findOne({ id });
|
||||
|
||||
// Get last heartbeat
|
||||
const heartbeat = await server
|
||||
.storage
|
||||
.store('smartyellow/webserviceheartbeat')
|
||||
.find({ webservice: id })
|
||||
.toArray();
|
||||
const lastBeat = heartbeat[heartbeat.length - 1];
|
||||
|
||||
// Encountered an error while checking status
|
||||
if (testResult.error) {
|
||||
server.error('status: error while checking status of ' + id);
|
||||
server.error(testResult);
|
||||
}
|
||||
|
||||
// Service is down
|
||||
else if (!testResult.serviceUp) {
|
||||
// Don't perform automatic actions if already done
|
||||
if ((lastBeat && lastBeat.down == false) || !lastBeat) {
|
||||
// Insert heartbeat if last one is not valid anymore
|
||||
try {
|
||||
server.storage.store('smartyellow/webserviceheartbeat').insert({
|
||||
id: makeId(10),
|
||||
down: true,
|
||||
webservice: id,
|
||||
testResult,
|
||||
date: new Date(),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.error('status: could not save web service heartbeat');
|
||||
server.error(err);
|
||||
}
|
||||
|
||||
// Send e-mail notification
|
||||
if (server.sendEmail && settings.emailSender && settings.emailRecipient) {
|
||||
try {
|
||||
const date = new Date().toLocaleString('en-GB', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'full',
|
||||
timeZone: 'Etc/UTC',
|
||||
});
|
||||
|
||||
await server.sendEmail({
|
||||
sender: settings.emailSender,
|
||||
to: settings.emailRecipient,
|
||||
subject: `[outage] ${service.name} is down`,
|
||||
body: `Hello,
|
||||
|
||||
As of ${date} UTC time, the service "${service.name}" does not meet the requirements for being
|
||||
considered as working.
|
||||
|
||||
Technical information containing the reason for this alert:
|
||||
${JSON.stringify(testResult, null, 2)}
|
||||
|
||||
Please always check this before taking action. This is an automated message.`,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.warn('status: could not send endpoint status notification e-mail');
|
||||
server.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Draft outage entry
|
||||
if (settings.draftOutageEntries) {
|
||||
try {
|
||||
server
|
||||
.storage
|
||||
.store('smartyellow/webserviceoutage')
|
||||
.insert({
|
||||
id: makeId(),
|
||||
name: {
|
||||
en: `[automatic] Outage for ${service.name.en}`,
|
||||
},
|
||||
state: 'concept',
|
||||
resolved: false,
|
||||
services: [ service.id ],
|
||||
tags: [ 'automatically created' ],
|
||||
notes: [ {
|
||||
date: new Date(),
|
||||
userId: 'system',
|
||||
text: `Automatically created outage. Reason: ${JSON.stringify(testResult, null, 2)}`,
|
||||
} ],
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.warn('status: could not automatically draft outage entry');
|
||||
server.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service up
|
||||
else {
|
||||
// Don't perform automatic actions if already done
|
||||
if ((lastBeat && lastBeat.down == true) || !lastBeat) {
|
||||
// Insert heartbeat if last one is not valid anymore
|
||||
try {
|
||||
await server.storage.store('smartyellow/webserviceheartbeat').insert({
|
||||
id: makeId(10),
|
||||
down: false,
|
||||
webservice: id,
|
||||
testResult,
|
||||
date: new Date(),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.warn('status: could not save web service heartbeat');
|
||||
server.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
// Friendly name
|
||||
@ -133,7 +274,7 @@ module.exports = {
|
||||
{ path: 'webservices.svelte',
|
||||
requires: [ 'seeServices', 'editServices' ],
|
||||
menu: {
|
||||
cluster: guiCluster,
|
||||
cluster: 'web service status',
|
||||
icon: icons.server,
|
||||
title: 'web services',
|
||||
},
|
||||
@ -142,7 +283,7 @@ module.exports = {
|
||||
{ path: 'webserviceoutages.svelte',
|
||||
requires: [ 'seeServices', 'editServices' ],
|
||||
menu: {
|
||||
cluster: guiCluster,
|
||||
cluster: 'web service status',
|
||||
icon: icons.outage,
|
||||
title: 'outages',
|
||||
},
|
||||
@ -150,7 +291,7 @@ module.exports = {
|
||||
|
||||
{ path: 'webservicedashboard.svelte',
|
||||
menu: {
|
||||
cluster: guiCluster,
|
||||
cluster: 'web service status',
|
||||
icon: icons.external,
|
||||
title: 'dashboard',
|
||||
},
|
||||
@ -191,6 +332,7 @@ module.exports = {
|
||||
|
||||
runtime.on('message', message => {
|
||||
if (message.error) {
|
||||
server.error('status: runtime error');
|
||||
server.error(message.error);
|
||||
}
|
||||
else if (message.outage) {
|
||||
@ -206,7 +348,108 @@ module.exports = {
|
||||
event: 'boot',
|
||||
order: 100,
|
||||
purpose: 'Start the websocket for the dashboard after boot',
|
||||
handler: () => createDashboardSocket(server),
|
||||
handler: () => {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let downIdsBefore = [];
|
||||
let downIdsAfter = [];
|
||||
|
||||
const mapService = (s, beat) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
cluster: s.cluster,
|
||||
lastBeat: beat,
|
||||
checked: s.lastChecked,
|
||||
});
|
||||
|
||||
server.ws({
|
||||
route: '/status/dashboard/socket',
|
||||
onOpen: async ws => {
|
||||
async function sendStatuses() {
|
||||
const services = await server.storage
|
||||
.store('smartyellow/webservice')
|
||||
.find({ public: true })
|
||||
.toArray();
|
||||
const heartbeats = await server.storage
|
||||
.store('smartyellow/webserviceheartbeat')
|
||||
.find({ webservice: { $in: services.map(s => s.id) } })
|
||||
.sort({ date: -1 })
|
||||
.toArray();
|
||||
|
||||
const servicesUp = [];
|
||||
const servicesDown = [];
|
||||
const servicesUnknown = [];
|
||||
downIdsAfter = [];
|
||||
|
||||
for (let service of services) {
|
||||
const beat = heartbeats.find(b => b.webservice === service.id);
|
||||
service = mapService(service, beat);
|
||||
|
||||
if (!beat) {
|
||||
servicesUnknown.push(service);
|
||||
}
|
||||
else if (beat.down) {
|
||||
servicesDown.push(service);
|
||||
downIdsAfter.push(service.id);
|
||||
}
|
||||
else {
|
||||
servicesUp.push(service);
|
||||
}
|
||||
}
|
||||
|
||||
const total = [
|
||||
...servicesUp,
|
||||
...servicesDown,
|
||||
...servicesUnknown,
|
||||
].length;
|
||||
|
||||
let newOutage = false;
|
||||
for (const id of downIdsAfter) {
|
||||
if (!downIdsBefore.includes(id)) {
|
||||
newOutage = true;
|
||||
}
|
||||
}
|
||||
downIdsBefore = JSON.parse(JSON.stringify(downIdsAfter));
|
||||
|
||||
try {
|
||||
if (newOutage) {
|
||||
ws.send(JSON.stringify({ cmd: 'bell' }));
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
cmd: 'data',
|
||||
servicesUp,
|
||||
servicesDown,
|
||||
servicesUnknown,
|
||||
total,
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sendStatuses();
|
||||
setInterval(sendStatuses, 5000);
|
||||
},
|
||||
onUpgrade: async () => ({ id: makeId(10) }),
|
||||
onMessage: async (ws, msg) => {
|
||||
msg = JSON.parse(decoder.decode(msg));
|
||||
|
||||
if (!msg || !msg.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.command) {
|
||||
case 'data':
|
||||
ws.send('data');
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
{ id: 'autotestOnSave',
|
||||
@ -219,6 +462,7 @@ module.exports = {
|
||||
runtime.send({ command: 'testOne', service: item });
|
||||
runtime.on('message', message => {
|
||||
if (message.error) {
|
||||
server.error('status: runtime error');
|
||||
server.error(message.error);
|
||||
}
|
||||
else if (message.outage) {
|
||||
@ -296,6 +540,7 @@ module.exports = {
|
||||
runtime.on('message', async message => {
|
||||
res.json(message);
|
||||
if (message.error) {
|
||||
server.error('status: runtime error');
|
||||
server.error(message.error);
|
||||
}
|
||||
else if (message.outage) {
|
||||
@ -558,14 +803,52 @@ module.exports = {
|
||||
{ route: '/status/dashboard',
|
||||
method: 'get',
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
if (!renderedDashboard) {
|
||||
renderedDashboard = await buildDashboard();
|
||||
renderedDashboard.globalCss = await readFile(
|
||||
__dirname + '/gui/dashboard/app.css'
|
||||
);
|
||||
if (!renderedDashboard) {
|
||||
// Build dashboard
|
||||
let cssOutput = '';
|
||||
|
||||
try {
|
||||
const bundle = await buildDeps.rollup({
|
||||
input: __dirname + '/gui/dashboard/index.js',
|
||||
plugins: [
|
||||
buildDeps.svelte({
|
||||
compilerOptions: {
|
||||
dev: false,
|
||||
generate: 'dom',
|
||||
},
|
||||
}),
|
||||
buildDeps.css({ output: style => cssOutput = style }),
|
||||
buildDeps.resolve({
|
||||
browser: true,
|
||||
dedupe: [ 'svelte' ],
|
||||
}),
|
||||
buildDeps.commonjs(),
|
||||
buildDeps.terser(),
|
||||
],
|
||||
});
|
||||
|
||||
const { output } = await bundle.generate({
|
||||
sourcemap: false,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
});
|
||||
|
||||
renderedDashboard = {
|
||||
map: output[0].map ? output[0].map.toUrl() : '',
|
||||
code: output[0].code,
|
||||
css: cssOutput,
|
||||
globalCss: await readFile(
|
||||
__dirname + '/gui/dashboard/app.css'
|
||||
),
|
||||
};
|
||||
}
|
||||
const dashboardHtml = minifyHtml(`
|
||||
catch (error) {
|
||||
server.error('status: error while building dashboard: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
const dashboardHtml = minifyHtml(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -584,11 +867,7 @@ module.exports = {
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
res.send(dashboardHtml);
|
||||
}
|
||||
catch (error) {
|
||||
server.error('could not compile web service status dashboard', error);
|
||||
}
|
||||
res.send(dashboardHtml);
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { rollup } = require('rollup');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const css = require('rollup-plugin-css-only');
|
||||
const { default: resolve } = require('@rollup/plugin-node-resolve');
|
||||
const svelte = require('rollup-plugin-svelte');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
|
||||
async function build() {
|
||||
let cssOutput = '';
|
||||
|
||||
try {
|
||||
const bundle = await rollup({
|
||||
input: __dirname + '/../../gui/dashboard/index.js',
|
||||
plugins: [
|
||||
// Svelte
|
||||
svelte({
|
||||
compilerOptions: {
|
||||
dev: false,
|
||||
generate: 'dom',
|
||||
},
|
||||
}),
|
||||
|
||||
// Extract CSS
|
||||
css({ output: style => cssOutput = style }),
|
||||
|
||||
// Resolve dependencies
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: [ 'svelte' ],
|
||||
}),
|
||||
|
||||
// CommonJS functions
|
||||
commonjs(),
|
||||
|
||||
// Minify
|
||||
terser(),
|
||||
],
|
||||
});
|
||||
|
||||
const { output } = await bundle.generate({
|
||||
sourcemap: false,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
});
|
||||
|
||||
return {
|
||||
map: output[0].map ? output[0].map.toUrl() : '',
|
||||
code: output[0].code,
|
||||
css: cssOutput,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error while building status dashboard: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = build;
|
@ -1,108 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { makeId } = require('core/makeid');
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let downIdsBefore = [];
|
||||
let downIdsAfter = [];
|
||||
|
||||
const mapService = (s, beat) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
cluster: s.cluster,
|
||||
lastBeat: beat,
|
||||
checked: s.lastChecked,
|
||||
});
|
||||
|
||||
async function createDashboardSocket(server) {
|
||||
server.ws({
|
||||
route: '/status/dashboard/socket',
|
||||
onOpen: async ws => {
|
||||
async function sendStatuses() {
|
||||
const services = await server.storage
|
||||
.store('smartyellow/webservice')
|
||||
.find({ public: true })
|
||||
.toArray();
|
||||
const heartbeats = await server.storage
|
||||
.store('smartyellow/webserviceheartbeat')
|
||||
.find({ webservice: { $in: services.map(s => s.id) } })
|
||||
.sort({ date: -1 })
|
||||
.toArray();
|
||||
|
||||
const servicesUp = [];
|
||||
const servicesDown = [];
|
||||
const servicesUnknown = [];
|
||||
downIdsAfter = [];
|
||||
|
||||
for (let service of services) {
|
||||
const beat = heartbeats.find(b => b.webservice === service.id);
|
||||
service = mapService(service, beat);
|
||||
|
||||
if (!beat) {
|
||||
servicesUnknown.push(service);
|
||||
}
|
||||
else if (beat.down) {
|
||||
servicesDown.push(service);
|
||||
downIdsAfter.push(service.id);
|
||||
}
|
||||
else {
|
||||
servicesUp.push(service);
|
||||
}
|
||||
}
|
||||
|
||||
const total = [
|
||||
...servicesUp,
|
||||
...servicesDown,
|
||||
...servicesUnknown,
|
||||
].length;
|
||||
|
||||
let newOutage = false;
|
||||
for (const id of downIdsAfter) {
|
||||
if (!downIdsBefore.includes(id)) {
|
||||
newOutage = true;
|
||||
}
|
||||
}
|
||||
downIdsBefore = JSON.parse(JSON.stringify(downIdsAfter));
|
||||
|
||||
try {
|
||||
if (newOutage) {
|
||||
ws.send(JSON.stringify({ cmd: 'bell' }));
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
cmd: 'data',
|
||||
servicesUp,
|
||||
servicesDown,
|
||||
servicesUnknown,
|
||||
total,
|
||||
}));
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sendStatuses();
|
||||
setInterval(sendStatuses, 5000);
|
||||
},
|
||||
onUpgrade: async () => ({ id: makeId(10) }),
|
||||
onMessage: async (ws, msg) => {
|
||||
msg = JSON.parse(decoder.decode(msg));
|
||||
|
||||
if (!msg || !msg.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.command) {
|
||||
case 'data':
|
||||
ws.send('data');
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = createDashboardSocket;
|
@ -1,141 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { makeId } = require('core/makeid');
|
||||
|
||||
async function processOutage({ outage, server, settings, onDateUpdated }) {
|
||||
if (typeof onDateUpdated !== 'function') {
|
||||
onDateUpdated = () => null;
|
||||
}
|
||||
|
||||
for (const [ id, testResult ] of Object.entries(outage)) {
|
||||
// Update check date
|
||||
server.storage.store('smartyellow/webservice').update(
|
||||
{ id },
|
||||
{ $set: { lastChecked: new Date() } }
|
||||
).then(() => onDateUpdated(id));
|
||||
|
||||
// Get service entry
|
||||
const service = await server
|
||||
.storage
|
||||
.store('smartyellow/webservice')
|
||||
.findOne({ id });
|
||||
|
||||
// Get last heartbeat
|
||||
const heartbeat = await server
|
||||
.storage
|
||||
.store('smartyellow/webserviceheartbeat')
|
||||
.find({ webservice: id })
|
||||
.toArray();
|
||||
const lastBeat = heartbeat[heartbeat.length - 1];
|
||||
|
||||
// Encountered an error while checking status
|
||||
if (testResult.error) {
|
||||
server.error('Error while checking status of ' + id);
|
||||
server.error(testResult);
|
||||
}
|
||||
|
||||
// Service is down
|
||||
else if (!testResult.serviceUp) {
|
||||
// Don't perform automatic actions if already done
|
||||
if ((lastBeat && lastBeat.down == false) || !lastBeat) {
|
||||
// Insert heartbeat if last one is not valid anymore
|
||||
try {
|
||||
server.storage.store('smartyellow/webserviceheartbeat').insert({
|
||||
id: makeId(10),
|
||||
down: true,
|
||||
webservice: id,
|
||||
testResult,
|
||||
date: new Date(),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.error('could not save web service heartbeat');
|
||||
server.error(err);
|
||||
}
|
||||
|
||||
// Send e-mail notification
|
||||
if (server.sendEmail && settings.emailSender && settings.emailRecipient) {
|
||||
try {
|
||||
const date = new Date().toLocaleString('en-GB', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'full',
|
||||
timeZone: 'Etc/UTC',
|
||||
});
|
||||
const a = await server.sendEmail({
|
||||
sender: settings.emailSender,
|
||||
to: settings.emailRecipient,
|
||||
subject: `[outage] ${service.name} is down`,
|
||||
body: `Dear recipient,
|
||||
|
||||
As of ${date} UTC time, the service "${service.name}" does not meet the requirements for being
|
||||
considered as working.
|
||||
|
||||
Technical information containing the reason for this alert:
|
||||
${JSON.stringify(testResult, null, 2)}
|
||||
|
||||
Please always check this before taking action. This is an automated message.`,
|
||||
});
|
||||
console.log(a);
|
||||
}
|
||||
catch (err) {
|
||||
server.error('could not send endpoint status notification e-mail');
|
||||
server.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Draft outage entry
|
||||
if (settings.draftOutageEntries) {
|
||||
try {
|
||||
server
|
||||
.storage
|
||||
.store('smartyellow/webserviceoutage')
|
||||
.insert({
|
||||
id: makeId(),
|
||||
name: {
|
||||
en: `[automatic] Outage for ${service.name.en}`,
|
||||
},
|
||||
state: 'concept',
|
||||
resolved: false,
|
||||
services: [ service.id ],
|
||||
tags: [ 'automatically created' ],
|
||||
notes: [ {
|
||||
date: new Date(),
|
||||
userId: 'system',
|
||||
text: `Automatically created outage. Reason: ${JSON.stringify(testResult, null, 2)}`,
|
||||
} ],
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.error('could not automatically draft outage entry');
|
||||
server.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Service up
|
||||
else {
|
||||
// Don't perform automatic actions if already done
|
||||
if ((lastBeat && lastBeat.down == true) || !lastBeat) {
|
||||
// Insert heartbeat if last one is not valid anymore
|
||||
try {
|
||||
await server.storage.store('smartyellow/webserviceheartbeat').insert({
|
||||
id: makeId(10),
|
||||
down: false,
|
||||
webservice: id,
|
||||
testResult,
|
||||
date: new Date(),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
server.error('could not save web service heartbeat');
|
||||
server.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
module.exports = { processOutage };
|
@ -1,6 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
const { testEndpoints } = require('./testendpoints');
|
||||
const fetch = require('node-fetch');
|
||||
const { operators } = require('./operators');
|
||||
const { realValues } = require('./realvalues');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
// Force requests over IPv4
|
||||
const httpAgent = new http.Agent({ family: 4 });
|
||||
const httpsAgent = new https.Agent({ family: 4 });
|
||||
|
||||
async function testEndpoints(endpoints) {
|
||||
const output = {
|
||||
serviceUp: undefined,
|
||||
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,
|
||||
agent: url => {
|
||||
if (url.protocol === 'http:') {
|
||||
return httpAgent;
|
||||
}
|
||||
return httpsAgent;
|
||||
},
|
||||
});
|
||||
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.serviceUp = false;
|
||||
output.requirement = requirement;
|
||||
output.realValue = realValue;
|
||||
}
|
||||
else {
|
||||
output.serviceUp = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
output.success = false;
|
||||
output.serviceUp = false;
|
||||
output.error = err;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
process.on('message', async message => {
|
||||
switch (message.command) {
|
||||
|
@ -1,83 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fetch = require('node-fetch');
|
||||
const { operators } = require('./operators');
|
||||
const { realValues } = require('./realvalues');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
// Force requests over IPv4
|
||||
const httpAgent = new http.Agent({ family: 4 });
|
||||
const httpsAgent = new https.Agent({ family: 4 });
|
||||
|
||||
async function testEndpoints(endpoints) {
|
||||
const output = {
|
||||
serviceUp: undefined,
|
||||
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,
|
||||
agent: url => {
|
||||
if (url.protocol === 'http:') {
|
||||
return httpAgent;
|
||||
}
|
||||
return httpsAgent;
|
||||
},
|
||||
});
|
||||
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.serviceUp = false;
|
||||
output.requirement = requirement;
|
||||
output.realValue = realValue;
|
||||
}
|
||||
else {
|
||||
output.serviceUp = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
output.success = false;
|
||||
output.serviceUp = false;
|
||||
output.error = err;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
module.exports = { testEndpoints };
|
Loading…
Reference in New Issue
Block a user