~rbdr/monitorcito

2d72aab0a88baa906127489e1c25fd11881bdc35 — Ruben Beltran del Rio 2 years ago
Add the monitor
A  => .gitignore +2 -0
@@ 1,2 @@
.env
node_modules

A  => README.md +48 -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;
	}
}
```

A  => bin/monitorcito +55 -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);
  });

A  => env.dist +1 -0
@@ 1,1 @@
MONITORCITO_SERVICES=comma,separated,list

A  => lib/index.js +44 -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);
};

A  => package-lock.json +5 -0
@@ 1,5 @@
{
  "name": "monitorcito",
  "version": "1.0.0",
  "lockfileVersion": 1
}

A  => package.json +19 -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": {}
}

A  => public/css/application.css +41 -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;
  }
}

A  => public/index.html +36 -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>

A  => public/js/monitorcito.js +40 -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();
}
)();