add statsd monitoring

wbo is now more observable
This commit is contained in:
lovasoa 2021-06-10 16:41:44 +02:00
parent 55acf500d0
commit fcc97f58b5
No known key found for this signature in database
GPG key ID: AC8DB8E033B44AB8
8 changed files with 3370 additions and 3276 deletions

View file

@ -90,3 +90,16 @@ Some important environment variables are :
## Troubleshooting
If you experience an issue or want to propose a new feature in WBO, please [open a github issue](https://github.com/lovasoa/whitebophir/issues/new).
## Monitoring
If you are self-hosting a WBO instance, you may want to monitor its load,
the number of connected users, and different metrics.
You can start WBO with the `STATSD_URL` to send it to a statsd-compatible
metrics collection agent.
Example: `docker run -e STATSD_URL=udp://127.0.0.1:8125 lovasoa/wbo`.
- If you use **prometheus**, you can collect the metrics with [statsd-exporter](https://hub.docker.com/r/prom/statsd-exporter).
- If you use **datadog**, you can collect the metrics with [dogstatsd](https://docs.datadoghq.com/developers/dogstatsd).

87
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "whitebophir",
"version": "1.11.0",
"version": "1.14.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -797,9 +797,9 @@
}
},
"es-abstract": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz",
"integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
@ -810,14 +810,14 @@
"has-symbols": "^1.0.2",
"is-callable": "^1.2.3",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.2",
"is-string": "^1.0.5",
"object-inspect": "^1.9.0",
"is-regex": "^1.1.3",
"is-string": "^1.0.6",
"object-inspect": "^1.10.3",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.4",
"string.prototype.trimstart": "^1.0.4",
"unbox-primitive": "^1.0.0"
"unbox-primitive": "^1.0.1"
},
"dependencies": {
"object.assign": {
@ -1334,12 +1334,12 @@
"dev": true
},
"is-boolean-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
"integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz",
"integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==",
"dev": true,
"requires": {
"call-bind": "^1.0.0"
"call-bind": "^1.0.2"
}
},
"is-buffer": {
@ -1355,9 +1355,9 @@
"dev": true
},
"is-date-object": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.3.tgz",
"integrity": "sha512-tDpEUInNcy2Yw3lNSepK3Wdw1RnXLcIVienz6Ou631Acl15cJyRWK4dgA1vCmOEgIbtOV0W7MHg+AR2Gdg1NXQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz",
"integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==",
"dev": true
},
"is-fullwidth-code-point": {
@ -1379,9 +1379,9 @@
"dev": true
},
"is-number-object": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz",
"integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
"dev": true
},
"is-plain-obj": {
@ -1397,13 +1397,13 @@
"dev": true
},
"is-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
"integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
"integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"has-symbols": "^1.0.1"
"has-symbols": "^1.0.2"
}
},
"is-retry-allowed": {
@ -1419,18 +1419,18 @@
"dev": true
},
"is-string": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
"integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
"integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==",
"dev": true
},
"is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
"integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
"has-symbols": "^1.0.2"
}
},
"is-typedarray": {
@ -1922,9 +1922,9 @@
"dev": true
},
"nightwatch": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-1.6.3.tgz",
"integrity": "sha512-otVr+YUmtXcj7aG14GfkuCMUOpuJfSOVvsTxQiSRmO3rSfclOWglE6jaCmyMiHMYNwy/LPp86PXJFD5pzMo/wA==",
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-1.6.4.tgz",
"integrity": "sha512-3Ivb8TVjM9GHYwN0Ixi0CK+5hznRLsY7iiY1CBmdRi01ofx3a6glVvbrhsfOshwN3ai/tTaJ2D9taQGZ05wy3w==",
"dev": true,
"requires": {
"assertion-error": "^1.1.0",
@ -1984,9 +1984,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-inspect": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz",
"integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
"dev": true
},
"object-keys": {
@ -2294,9 +2294,9 @@
}
},
"polyfill-library": {
"version": "3.104.0",
"resolved": "https://registry.npmjs.org/polyfill-library/-/polyfill-library-3.104.0.tgz",
"integrity": "sha512-sYbO07WkGKSub4HxUKK/8eYSxV/HS4xOUcexmV/zBl6Ma2JJypKWk1xgbcZi2Gs4CmPrZIpfGhdPK1aFXjJKtQ==",
"version": "3.105.0",
"resolved": "https://registry.npmjs.org/polyfill-library/-/polyfill-library-3.105.0.tgz",
"integrity": "sha512-Bt10kl+5I/k+F8U0/HYEw2RiHUyUYGz7KtzwhPVltngxcJjzm0pWTQi2Z8pYMI5ahE5B5WZv8CXgCVDxz/H4UA==",
"requires": {
"@financial-times/polyfill-useragent-normaliser": "^1.7.0",
"@formatjs/intl-datetimeformat": "3.2.9",
@ -2788,6 +2788,11 @@
"tweetnacl": "~0.14.0"
}
},
"statsd-client": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/statsd-client/-/statsd-client-0.4.7.tgz",
"integrity": "sha512-+sGCE6FednJ/vI7vywErOg/mhVqmf6Zlktz7cdGRnF/cQWXD9ifMgtqU1CIIXmhSwm11SCk4zDN+bwNCvIR/Kg=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@ -3192,9 +3197,9 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
},
"xregexp": {
"version": "2.0.0",

View file

@ -11,9 +11,10 @@
"accept-language-parser": "^1.5.0",
"async-mutex": "^0.3.1",
"handlebars": "^4.7.7",
"polyfill-library": "^3.104.0",
"polyfill-library": "^3.105.0",
"serve-static": "^1.14.1",
"socket.io": "^3.1.2"
"socket.io": "^3.1.2",
"statsd-client": "^0.4.7"
},
"scripts": {
"start": "node ./server/server.js",
@ -26,6 +27,6 @@
},
"devDependencies": {
"geckodriver": "^1.22.3",
"nightwatch": "^1.6.3"
"nightwatch": "^1.6.4"
}
}

View file

@ -215,7 +215,7 @@ class BoardData {
// empty board
try {
await fs.promises.unlink(file);
log("removed empty board", { name: this.name });
log("removed empty board", { board: this.name });
} catch (err) {
if (err.code !== "ENOENT") {
// If the file already wasn't saved, this is not an error
@ -227,12 +227,13 @@ class BoardData {
await fs.promises.writeFile(tmp_file, board_txt, { flag: "wx" });
await fs.promises.rename(tmp_file, file);
log("saved board", {
name: this.name,
board: this.name,
size: board_txt.length,
delay_ms: Date.now() - this.lastSaveDate,
});
} catch (err) {
log("board saving error", {
board: this.name,
err: err.toString(),
tmp_file: tmp_file,
});

View file

@ -47,5 +47,11 @@ module.exports = {
/** Automatically switch to White-out on finger touch after drawing
with Pencil using a stylus. Only supported on iPad with Apple Pencil. */
AUTO_FINGER_WHITEOUT: process.env['AUTO_FINGER_WHITEOUT'] !== "disabled",
AUTO_FINGER_WHITEOUT: process.env["AUTO_FINGER_WHITEOUT"] !== "disabled",
/** If this variable is set, it should point to a statsd listener that will
* receive WBO's monitoring information.
* example: udp://127.0.0.1
*/
STATSD_URL: process.env["STATSD_URL"],
};

View file

@ -1,3 +1,40 @@
const config = require("./configuration.js"),
SDC = require("statsd-client");
/**
* Parse a statsd connection string
* @param {string} url
* @returns {SDC.TcpOptions|SDC.UdpOptions}
*/
function parse_statsd_url(url) {
const regex = /^(tcp|udp|statsd):\/\/(.*):(\d+)$/;
const match = url.match(regex);
if (!match)
throw new Error("Invalid statsd connection string, doesn't match " + regex);
const [_, protocol, host, port_str] = match;
const tcp = protocol !== "udp";
const port = parseInt(port_str);
return { tcp, host, port, prefix: "wbo" };
}
/**
* Statsd client to which metrics will be reported
* @type {SDC | null}
* */
let statsd = null;
if (config.STATSD_URL) {
const options = parse_statsd_url(config.STATSD_URL);
console.log("Exposing metrics on statsd server: " + JSON.stringify(options));
statsd = new SDC(options);
}
if (statsd) {
setInterval(function reportHealth(){
statsd.gauge('memory', process.memoryUsage().heapUsed);
}, 1000);
}
/**
* Add a message to the logs
* @param {string} type
@ -6,7 +43,33 @@
function log(type, infos) {
var msg = type;
if (infos) msg += "\t" + JSON.stringify(infos);
if (statsd) {
const tags = {};
if (infos.board) tags.board = infos.board;
if (infos.original_ip) tags.original_ip = infos.original_ip;
statsd.increment(type, 1, tags);
}
console.log(msg);
}
module.exports.log = log;
/**
* @template {(...args) => any} F
* @param {F} f
* @returns {F}
*/
function monitorFunction(f) {
if (statsd) return statsd.helpers.wrapCallback(f.name, f);
else return f;
}
/**
* Report a number
* @param {string} name
* @param {number} value
* @param {{[name:string]: string}=} tags
*/
function gauge(name, value, tags){
if (statsd) statsd.gauge(name, value, tags);
}
module.exports = { log, gauge, monitorFunction };

View file

@ -1,8 +1,7 @@
var app = require("http").createServer(handler),
sockets = require("./sockets.js"),
log = require("./log.js").log,
{log, monitorFunction} = require("./log.js"),
path = require("path"),
url = require("url"),
fs = require("fs"),
crypto = require("crypto"),
serveStatic = require("serve-static"),
@ -57,7 +56,7 @@ function serveError(request, response) {
*/
function logRequest(request) {
log("connection", {
ip: request.connection.remoteAddress,
ip: request.socket.remoteAddress,
original_ip:
request.headers["x-forwarded-for"] || request.headers["forwarded"],
user_agent: request.headers["user-agent"],
@ -72,7 +71,7 @@ function logRequest(request) {
*/
function handler(request, response) {
try {
handleRequest(request, response);
handleRequestAndLog(request, response);
} catch (err) {
console.trace(err);
response.writeHead(500, { "Content-Type": "text/plain" });
@ -101,7 +100,7 @@ function validateBoardName(boardName) {
* @type {import('http').RequestListener}
*/
function handleRequest(request, response) {
var parsedUrl = url.parse(request.url, true);
var parsedUrl = new URL(request.url, 'http://wbo/');
var parts = parsedUrl.pathname.split("/");
if (parts[0] === "") parts.shift();
@ -110,7 +109,7 @@ function handleRequest(request, response) {
// "boards" refers to the root directory
if (parts.length === 1) {
// '/boards?board=...' This allows html forms to point to boards
var boardName = parsedUrl.query.board || "anonymous";
var boardName = parsedUrl.searchParams.get("board") || "anonymous";
var headers = { Location: "boards/" + encodeURIComponent(boardName) };
response.writeHead(301, headers);
response.end();
@ -218,4 +217,5 @@ function handleRequest(request, response) {
}
}
const handleRequestAndLog = monitorFunction(handleRequest);
module.exports = app;

View file

@ -1,5 +1,5 @@
var iolib = require("socket.io"),
log = require("./log.js").log,
{ log, gauge, monitorFunction } = require("./log.js"),
BoardData = require("./boardData.js").BoardData,
config = require("./configuration");
@ -17,9 +17,10 @@ var boards = {};
* @returns {A}
*/
function noFail(fn) {
const monitored = monitorFunction(fn);
return function noFailWrapped(arg) {
try {
return fn(arg);
return monitored(arg);
} catch (e) {
console.trace(e);
}
@ -28,7 +29,7 @@ function noFail(fn) {
function startIO(app) {
io = iolib(app);
io.on("connection", noFail(socketConnection));
io.on("connection", noFail(handleSocketConnection));
return io;
}
@ -41,6 +42,7 @@ function getBoard(name) {
} else {
var board = BoardData.load(name);
boards[name] = board;
gauge("boards in memory", Object.keys(boards).length);
return board;
}
}
@ -49,7 +51,7 @@ function getBoard(name) {
* Executes on every new connection
* @param {iolib.Socket} socket
*/
function socketConnection(socket) {
function handleSocketConnection(socket) {
/**
* Function to call when an user joins a board
* @param {string} name
@ -64,6 +66,7 @@ function socketConnection(socket) {
var board = await getBoard(name);
board.users.add(socket.id);
log("board joined", { board: board.name, users: board.users.size });
gauge("connected", board.users.size, {board: name});
return board;
}
@ -141,9 +144,11 @@ function socketConnection(socket) {
board.users.delete(socket.id);
var userCount = board.users.size;
log("disconnection", { board: board.name, users: board.users.size });
gauge("connected", userCount, { board: board.name });
if (userCount === 0) {
board.save();
delete boards[room];
gauge("boards in memory", Object.keys(boards).length);
}
}
});