]> git.r.bdr.sh - rbdr/monitorcito/commitdiff
Add the monitor
authorRuben Beltran del Rio <redacted>
Tue, 8 Jun 2021 19:37:55 +0000 (21:37 +0200)
committerRuben Beltran del Rio <redacted>
Tue, 8 Jun 2021 19:37:55 +0000 (21:37 +0200)
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
bin/monitorcito [new file with mode: 0755]
env.dist [new file with mode: 0644]
lib/index.js [new file with mode: 0644]
package-lock.json [new file with mode: 0644]
package.json [new file with mode: 0644]
public/css/application.css [new file with mode: 0644]
public/index.html [new file with mode: 0644]
public/js/monitorcito.js [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..6ed48a9
--- /dev/null
@@ -0,0 +1,2 @@
+.env
+node_modules
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..807c1f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,48 @@
+# Monitorcito
+
+A tiny monitor that gives you the status of a handful of systemd services.
+
+## Configuration
+
+This project uses environment variables to change configuration.
+
+* `MONITORCITO_SERVICES`: A comma separated list of services. Required.
+* `MONITORCITO_PORT`: A comma separated list of services. Defaults to 1991.
+
+
+Set `NODE_DEBUG=monitorcito` if you want to log any output. Otherwise it will
+be silent.
+
+## How to run
+
+Set the appropriate environment variables and run `bin/monitorcito`.
+
+## The Frontend
+
+Monitorcito includes a public directory, this is a frontend that queries
+the service and refreshes every 5 seconds. The service assumes the API
+is running in `/api`.
+
+Here's an example of how to set it up with nginx.
+
+```
+upstream monitorcito {
+       server localhost:1991;
+       keepalive 64;
+}
+
+server {
+       listen 80;
+
+       root /home/deploy/src/monitorcito/public;
+       index index.html;
+
+       server_name monitor.unlimited.pizza;
+
+       location /api {
+               proxy_redirect off;
+
+               proxy_pass http://monitorcito;
+       }
+}
+```
diff --git a/bin/monitorcito b/bin/monitorcito
new file mode 100755 (executable)
index 0000000..2d41152
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env node
+
+const Monitorcito = require('..');
+const Http = require('http');
+const { debuglog } = require('util');
+
+const internals = {
+
+  kUnsetServicesError: 'Please specify comma separated services in MONITORCITO_SERVICES env variable',
+
+  arguments: null,
+  log: debuglog('monitorcito'),
+
+  prepareArguments() {
+
+    internals.log('Validating arguments');
+    if (!process.env.MONITORCITO_SERVICES) {
+      throw new Error(internals.kUnsetServicesError);
+    }
+
+    internals.arguments = process.env.MONITORCITO_SERVICES.split(',')
+    internals.log(`Arguments are ${internals.arguments}`);
+  },
+
+  startServer() {
+
+    internals.log('Setting up the server');
+    const server = Http.createServer(async (request, response) => {
+
+      internals.log('Incoming request');
+      const responseBody = JSON.stringify(await Monitorcito(internals.arguments));
+      internals.log(`Responding with ${responseBody}`);
+
+      response.writeHead(200, { 'Content-Type': 'application/json' });
+      response.write(responseBody);
+      response.end();
+    });
+    const port = Number(process.env.MONITORCITO_PORT) || 1991;
+    server.listen(port);
+    internals.log(`Listening on port ${port}`);
+  },
+
+  async run() {
+
+    internals.prepareArguments();
+    internals.startServer();
+  }
+};
+
+internals.run()
+  .catch((error) => {
+
+    console.error(error);
+    process.exit(1);
+  });
diff --git a/env.dist b/env.dist
new file mode 100644 (file)
index 0000000..6a1d0f6
--- /dev/null
+++ b/env.dist
@@ -0,0 +1 @@
+MONITORCITO_SERVICES=comma,separated,list
diff --git a/lib/index.js b/lib/index.js
new file mode 100644 (file)
index 0000000..d5d9668
--- /dev/null
@@ -0,0 +1,44 @@
+const { promisify } = require('util');
+const { exec } = require('child_process');
+
+const internals = {
+  kActiveIndicator: 'active',
+  kFieldSeparator: '\n',
+  kKeyValueSeparator: '=',
+  kBlockSeparator: '\n\n',
+  statusCommand: 'systemctl show --no-page -p Id -p ActiveState',
+
+  formatStatusOutput(systemctlText) {
+
+    const blocks = systemctlText.trim().split(internals.kBlockSeparator);
+    return blocks
+      .map((block) => {
+
+        const fields = block.split(internals.kFieldSeparator);
+        return fields.reduce((fieldObject, field) => {
+
+          const [key, value] = field.split(internals.kKeyValueSeparator);
+          return {
+            ...fieldObject,
+            [key]: value
+          }
+        }, {})
+
+      })
+      .reduce((statusObject, service) => {
+
+        return {
+          ...statusObject,
+          [service.Id]: service.ActiveState === internals.kActiveIndicator
+        }
+      }, {});
+  },
+
+  exec: promisify(exec)
+};
+
+module.exports = async (services) => {
+
+  const { stdout } = await internals.exec(`${internals.statusCommand} ${services.join(' ')}`);
+  return internals.formatStatusOutput(stdout);
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..04670a8
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "name": "monitorcito",
+  "version": "1.0.0",
+  "lockfileVersion": 1
+}
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..68baafa
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "name": "monitorcito",
+  "version": "1.0.0",
+  "description": "A tiny web monitor for systems",
+  "main": "lib/index.js",
+  "directories": {
+    "lib": "lib"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [
+    "systemd",
+    "monitor"
+  ],
+  "author": "Rubén Beltrán del Río <apps@unlimited.pizza>",
+  "license": "Apache-2.0",
+  "dependencies": {}
+}
diff --git a/public/css/application.css b/public/css/application.css
new file mode 100644 (file)
index 0000000..094313c
--- /dev/null
@@ -0,0 +1,41 @@
+h1 canvas {
+  width: 64px;
+  height: 64px;
+  display: inline-block;
+  background-color: gainsboro;
+}
+
+table {
+  border-collapse: collapse;
+}
+
+th, td {
+  padding: 8px;
+  border-style: inset;
+}
+
+.true {
+  color: green;
+}
+.false {
+  color: red;
+}
+.critical {
+  font-weight: bold;
+  color: red;
+}
+
+@media (prefers-color-scheme: dark) {
+  body {
+    color: white;
+    background-color: black;
+  }
+
+  a {
+    color: #5dc1fd;
+  }
+
+  a:visited {
+    color: #ed6eff;
+  }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644 (file)
index 0000000..6e673be
--- /dev/null
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="author" content="Rubén Beltrán del Río">
+    <meta name="description" content="monitor @ unlimited.pizza - check the status of running services">
+
+    <title>monitor @ unlimited &#127829;</title>
+
+    <link rel="stylesheet" type="text/css" href="/css/application.css">
+    <script type="module" src="https://unlimited.pizza/js/animation.js"></script>
+    <script type="module" src="/js/monitorcito.js"></script>
+
+    <!--
+        /\
+       / O\ U N L I M I T E D
+      /O o \    P I Z Z A
+     |______|
+    -->
+  </head>
+  <body>
+    <header>
+      <h1>
+        <canvas width=100 height=100></canvas>
+        <a href="/">Monitorcito</a>
+      </h1>
+    </header>
+    <main>
+      <p>Status of services running in unlimited dot pizza</p>
+      <table id="monitorcito">
+      </table>
+    </main>
+  </body>
+</html>
diff --git a/public/js/monitorcito.js b/public/js/monitorcito.js
new file mode 100644 (file)
index 0000000..ef428a5
--- /dev/null
@@ -0,0 +1,40 @@
+(function () {
+
+  const table = document.querySelector('#monitorcito');
+
+  function loadData () {
+    fetch('/api')
+    .then((response) => response.json())
+    .then((serviceStatus) => {
+
+      while (table.firstChild) {
+        table.removeChild(table.firstChild)
+      }
+
+      for (const [service, isActive] of Object.entries(serviceStatus)) {
+        const row = document.createElement('tr');
+        const serviceCell = document.createElement('td');
+        serviceCell.appendChild(document.createTextNode(service));
+        const isActiveCell = document.createElement('td');
+        isActiveCell.appendChild(document.createTextNode(isActive ? 'OK' : 'FAIL'));
+        isActiveCell.classList.add(isActive);
+        row.appendChild(serviceCell);
+        row.appendChild(isActiveCell);
+        table.appendChild(row);
+      }
+
+      setTimeout(loadData, 5000);
+    })
+    .catch(() => {
+      const container = document.querySelector('main');
+      container.removeChild(table);
+      const paragraph = document.createElement('p');
+      paragraph.appendChild(document.createTextNode('Everything is broken. If your internet works, please check the server\'s not on fire. When in doubt, just refresh.'));
+      paragraph.classList.add('critical');
+      container.appendChild(paragraph);
+    })
+  }
+
+  loadData();
+}
+)();