mirror of
https://github.com/lovasoa/whitebophir
synced 2024-11-10 14:34:20 +00:00
add statsd monitoring
wbo is now more observable
This commit is contained in:
parent
55acf500d0
commit
fcc97f58b5
8 changed files with 3370 additions and 3276 deletions
13
README.md
13
README.md
|
@ -90,3 +90,16 @@ Some important environment variables are :
|
||||||
## Troubleshooting
|
## 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).
|
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).
|
6523
package-lock.json
generated
6523
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,9 +11,10 @@
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"async-mutex": "^0.3.1",
|
"async-mutex": "^0.3.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"polyfill-library": "^3.104.0",
|
"polyfill-library": "^3.105.0",
|
||||||
"serve-static": "^1.14.1",
|
"serve-static": "^1.14.1",
|
||||||
"socket.io": "^3.1.2"
|
"socket.io": "^3.1.2",
|
||||||
|
"statsd-client": "^0.4.7"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./server/server.js",
|
"start": "node ./server/server.js",
|
||||||
|
@ -26,6 +27,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"geckodriver": "^1.22.3",
|
"geckodriver": "^1.22.3",
|
||||||
"nightwatch": "^1.6.3"
|
"nightwatch": "^1.6.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,7 @@ class BoardData {
|
||||||
// empty board
|
// empty board
|
||||||
try {
|
try {
|
||||||
await fs.promises.unlink(file);
|
await fs.promises.unlink(file);
|
||||||
log("removed empty board", { name: this.name });
|
log("removed empty board", { board: this.name });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code !== "ENOENT") {
|
if (err.code !== "ENOENT") {
|
||||||
// If the file already wasn't saved, this is not an error
|
// 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.writeFile(tmp_file, board_txt, { flag: "wx" });
|
||||||
await fs.promises.rename(tmp_file, file);
|
await fs.promises.rename(tmp_file, file);
|
||||||
log("saved board", {
|
log("saved board", {
|
||||||
name: this.name,
|
board: this.name,
|
||||||
size: board_txt.length,
|
size: board_txt.length,
|
||||||
delay_ms: Date.now() - this.lastSaveDate,
|
delay_ms: Date.now() - this.lastSaveDate,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("board saving error", {
|
log("board saving error", {
|
||||||
|
board: this.name,
|
||||||
err: err.toString(),
|
err: err.toString(),
|
||||||
tmp_file: tmp_file,
|
tmp_file: tmp_file,
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,5 +47,11 @@ module.exports = {
|
||||||
|
|
||||||
/** Automatically switch to White-out on finger touch after drawing
|
/** Automatically switch to White-out on finger touch after drawing
|
||||||
with Pencil using a stylus. Only supported on iPad with Apple Pencil. */
|
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"],
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
* Add a message to the logs
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
|
@ -6,7 +43,33 @@
|
||||||
function log(type, infos) {
|
function log(type, infos) {
|
||||||
var msg = type;
|
var msg = type;
|
||||||
if (infos) msg += "\t" + JSON.stringify(infos);
|
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);
|
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 };
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
var app = require("http").createServer(handler),
|
var app = require("http").createServer(handler),
|
||||||
sockets = require("./sockets.js"),
|
sockets = require("./sockets.js"),
|
||||||
log = require("./log.js").log,
|
{log, monitorFunction} = require("./log.js"),
|
||||||
path = require("path"),
|
path = require("path"),
|
||||||
url = require("url"),
|
|
||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
crypto = require("crypto"),
|
crypto = require("crypto"),
|
||||||
serveStatic = require("serve-static"),
|
serveStatic = require("serve-static"),
|
||||||
|
@ -57,7 +56,7 @@ function serveError(request, response) {
|
||||||
*/
|
*/
|
||||||
function logRequest(request) {
|
function logRequest(request) {
|
||||||
log("connection", {
|
log("connection", {
|
||||||
ip: request.connection.remoteAddress,
|
ip: request.socket.remoteAddress,
|
||||||
original_ip:
|
original_ip:
|
||||||
request.headers["x-forwarded-for"] || request.headers["forwarded"],
|
request.headers["x-forwarded-for"] || request.headers["forwarded"],
|
||||||
user_agent: request.headers["user-agent"],
|
user_agent: request.headers["user-agent"],
|
||||||
|
@ -72,7 +71,7 @@ function logRequest(request) {
|
||||||
*/
|
*/
|
||||||
function handler(request, response) {
|
function handler(request, response) {
|
||||||
try {
|
try {
|
||||||
handleRequest(request, response);
|
handleRequestAndLog(request, response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
response.writeHead(500, { "Content-Type": "text/plain" });
|
response.writeHead(500, { "Content-Type": "text/plain" });
|
||||||
|
@ -101,7 +100,7 @@ function validateBoardName(boardName) {
|
||||||
* @type {import('http').RequestListener}
|
* @type {import('http').RequestListener}
|
||||||
*/
|
*/
|
||||||
function handleRequest(request, response) {
|
function handleRequest(request, response) {
|
||||||
var parsedUrl = url.parse(request.url, true);
|
var parsedUrl = new URL(request.url, 'http://wbo/');
|
||||||
var parts = parsedUrl.pathname.split("/");
|
var parts = parsedUrl.pathname.split("/");
|
||||||
if (parts[0] === "") parts.shift();
|
if (parts[0] === "") parts.shift();
|
||||||
|
|
||||||
|
@ -110,7 +109,7 @@ function handleRequest(request, response) {
|
||||||
// "boards" refers to the root directory
|
// "boards" refers to the root directory
|
||||||
if (parts.length === 1) {
|
if (parts.length === 1) {
|
||||||
// '/boards?board=...' This allows html forms to point to boards
|
// '/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) };
|
var headers = { Location: "boards/" + encodeURIComponent(boardName) };
|
||||||
response.writeHead(301, headers);
|
response.writeHead(301, headers);
|
||||||
response.end();
|
response.end();
|
||||||
|
@ -218,4 +217,5 @@ function handleRequest(request, response) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRequestAndLog = monitorFunction(handleRequest);
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
var iolib = require("socket.io"),
|
var iolib = require("socket.io"),
|
||||||
log = require("./log.js").log,
|
{ log, gauge, monitorFunction } = require("./log.js"),
|
||||||
BoardData = require("./boardData.js").BoardData,
|
BoardData = require("./boardData.js").BoardData,
|
||||||
config = require("./configuration");
|
config = require("./configuration");
|
||||||
|
|
||||||
|
@ -17,9 +17,10 @@ var boards = {};
|
||||||
* @returns {A}
|
* @returns {A}
|
||||||
*/
|
*/
|
||||||
function noFail(fn) {
|
function noFail(fn) {
|
||||||
|
const monitored = monitorFunction(fn);
|
||||||
return function noFailWrapped(arg) {
|
return function noFailWrapped(arg) {
|
||||||
try {
|
try {
|
||||||
return fn(arg);
|
return monitored(arg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.trace(e);
|
console.trace(e);
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,7 @@ function noFail(fn) {
|
||||||
|
|
||||||
function startIO(app) {
|
function startIO(app) {
|
||||||
io = iolib(app);
|
io = iolib(app);
|
||||||
io.on("connection", noFail(socketConnection));
|
io.on("connection", noFail(handleSocketConnection));
|
||||||
return io;
|
return io;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ function getBoard(name) {
|
||||||
} else {
|
} else {
|
||||||
var board = BoardData.load(name);
|
var board = BoardData.load(name);
|
||||||
boards[name] = board;
|
boards[name] = board;
|
||||||
|
gauge("boards in memory", Object.keys(boards).length);
|
||||||
return board;
|
return board;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +51,7 @@ function getBoard(name) {
|
||||||
* Executes on every new connection
|
* Executes on every new connection
|
||||||
* @param {iolib.Socket} socket
|
* @param {iolib.Socket} socket
|
||||||
*/
|
*/
|
||||||
function socketConnection(socket) {
|
function handleSocketConnection(socket) {
|
||||||
/**
|
/**
|
||||||
* Function to call when an user joins a board
|
* Function to call when an user joins a board
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
|
@ -64,6 +66,7 @@ function socketConnection(socket) {
|
||||||
var board = await getBoard(name);
|
var board = await getBoard(name);
|
||||||
board.users.add(socket.id);
|
board.users.add(socket.id);
|
||||||
log("board joined", { board: board.name, users: board.users.size });
|
log("board joined", { board: board.name, users: board.users.size });
|
||||||
|
gauge("connected", board.users.size, {board: name});
|
||||||
return board;
|
return board;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,9 +144,11 @@ function socketConnection(socket) {
|
||||||
board.users.delete(socket.id);
|
board.users.delete(socket.id);
|
||||||
var userCount = board.users.size;
|
var userCount = board.users.size;
|
||||||
log("disconnection", { board: board.name, users: board.users.size });
|
log("disconnection", { board: board.name, users: board.users.size });
|
||||||
|
gauge("connected", userCount, { board: board.name });
|
||||||
if (userCount === 0) {
|
if (userCount === 0) {
|
||||||
board.save();
|
board.save();
|
||||||
delete boards[room];
|
delete boards[room];
|
||||||
|
gauge("boards in memory", Object.keys(boards).length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue