Sfoglia il codice sorgente

add statsd monitoring

wbo is now more observable
dev_h
lovasoa 4 anni fa
parent
commit
fcc97f58b5
Nessun account collegato all'indirizzo email del committer
8 ha cambiato i file con 3378 aggiunte e 3284 eliminazioni
  1. 13
    0
      README.md
  2. 3272
    3267
      package-lock.json
  3. 4
    3
      package.json
  4. 3
    2
      server/boardData.js
  5. 7
    1
      server/configuration.js
  6. 64
    1
      server/log.js
  7. 6
    6
      server/server.js
  8. 9
    4
      server/sockets.js

+ 13
- 0
README.md Vedi File

@@ -90,3 +90,16 @@ Some important environment variables are :
90 90
 ## Troubleshooting
91 91
 
92 92
 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).
93
+
94
+## Monitoring
95
+
96
+If you are self-hosting a WBO instance, you may want to monitor its load,
97
+the number of connected users, and different metrics.
98
+
99
+You can start WBO with the `STATSD_URL` to send it to a statsd-compatible
100
+metrics collection agent.
101
+
102
+Example: `docker run -e STATSD_URL=udp://127.0.0.1:8125 lovasoa/wbo`.
103
+
104
+ - If you use **prometheus**, you can collect the metrics with [statsd-exporter](https://hub.docker.com/r/prom/statsd-exporter).
105
+ - If you use **datadog**, you can collect the metrics with [dogstatsd](https://docs.datadoghq.com/developers/dogstatsd).

+ 3272
- 3267
package-lock.json
File diff soppresso perché troppo grande
Vedi File


+ 4
- 3
package.json Vedi File

@@ -11,9 +11,10 @@
11 11
     "accept-language-parser": "^1.5.0",
12 12
     "async-mutex": "^0.3.1",
13 13
     "handlebars": "^4.7.7",
14
-    "polyfill-library": "^3.104.0",
14
+    "polyfill-library": "^3.105.0",
15 15
     "serve-static": "^1.14.1",
16
-    "socket.io": "^3.1.2"
16
+    "socket.io": "^3.1.2",
17
+    "statsd-client": "^0.4.7"
17 18
   },
18 19
   "scripts": {
19 20
     "start": "node ./server/server.js",
@@ -26,6 +27,6 @@
26 27
   },
27 28
   "devDependencies": {
28 29
     "geckodriver": "^1.22.3",
29
-    "nightwatch": "^1.6.3"
30
+    "nightwatch": "^1.6.4"
30 31
   }
31 32
 }

+ 3
- 2
server/boardData.js Vedi File

@@ -215,7 +215,7 @@ class BoardData {
215 215
       // empty board
216 216
       try {
217 217
         await fs.promises.unlink(file);
218
-        log("removed empty board", { name: this.name });
218
+        log("removed empty board", { board: this.name });
219 219
       } catch (err) {
220 220
         if (err.code !== "ENOENT") {
221 221
           // If the file already wasn't saved, this is not an error
@@ -227,12 +227,13 @@ class BoardData {
227 227
         await fs.promises.writeFile(tmp_file, board_txt, { flag: "wx" });
228 228
         await fs.promises.rename(tmp_file, file);
229 229
         log("saved board", {
230
-          name: this.name,
230
+          board: this.name,
231 231
           size: board_txt.length,
232 232
           delay_ms: Date.now() - this.lastSaveDate,
233 233
         });
234 234
       } catch (err) {
235 235
         log("board saving error", {
236
+          board: this.name,
236 237
           err: err.toString(),
237 238
           tmp_file: tmp_file,
238 239
         });

+ 7
- 1
server/configuration.js Vedi File

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

+ 64
- 1
server/log.js Vedi File

@@ -1,3 +1,40 @@
1
+const config = require("./configuration.js"),
2
+  SDC = require("statsd-client");
3
+
4
+/**
5
+ * Parse a statsd connection string
6
+ * @param {string} url
7
+ * @returns {SDC.TcpOptions|SDC.UdpOptions}
8
+ */
9
+function parse_statsd_url(url) {
10
+  const regex = /^(tcp|udp|statsd):\/\/(.*):(\d+)$/;
11
+  const match = url.match(regex);
12
+  if (!match)
13
+    throw new Error("Invalid statsd connection string, doesn't match " + regex);
14
+  const [_, protocol, host, port_str] = match;
15
+  const tcp = protocol !== "udp";
16
+  const port = parseInt(port_str);
17
+  return { tcp, host, port, prefix: "wbo" };
18
+}
19
+
20
+/**
21
+ * Statsd client to which metrics will be reported
22
+ * @type {SDC | null}
23
+ * */
24
+let statsd = null;
25
+
26
+if (config.STATSD_URL) {
27
+  const options = parse_statsd_url(config.STATSD_URL);
28
+  console.log("Exposing metrics on statsd server: " + JSON.stringify(options));
29
+  statsd = new SDC(options);
30
+}
31
+
32
+if (statsd) {
33
+  setInterval(function reportHealth(){
34
+    statsd.gauge('memory', process.memoryUsage().heapUsed);
35
+  }, 1000);
36
+}
37
+
1 38
 /**
2 39
  * Add a message to the logs
3 40
  * @param {string} type
@@ -6,7 +43,33 @@
6 43
 function log(type, infos) {
7 44
   var msg = type;
8 45
   if (infos) msg += "\t" + JSON.stringify(infos);
46
+  if (statsd) {
47
+    const tags = {};
48
+    if (infos.board) tags.board = infos.board;
49
+    if (infos.original_ip) tags.original_ip = infos.original_ip;
50
+    statsd.increment(type, 1, tags);
51
+  }
9 52
   console.log(msg);
10 53
 }
11 54
 
12
-module.exports.log = log;
55
+/**
56
+ * @template {(...args) => any} F
57
+ * @param {F} f
58
+ * @returns {F}
59
+ */
60
+function monitorFunction(f) {
61
+  if (statsd) return statsd.helpers.wrapCallback(f.name, f);
62
+  else return f;
63
+}
64
+
65
+/**
66
+ * Report a number
67
+ * @param {string} name 
68
+ * @param {number} value 
69
+ * @param {{[name:string]: string}=} tags
70
+ */
71
+function gauge(name, value, tags){
72
+  if (statsd) statsd.gauge(name, value, tags);
73
+}
74
+
75
+module.exports = { log, gauge, monitorFunction };

+ 6
- 6
server/server.js Vedi File

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

+ 9
- 4
server/sockets.js Vedi File

@@ -1,5 +1,5 @@
1 1
 var iolib = require("socket.io"),
2
-  log = require("./log.js").log,
2
+  { log, gauge, monitorFunction } = require("./log.js"),
3 3
   BoardData = require("./boardData.js").BoardData,
4 4
   config = require("./configuration");
5 5
 
@@ -17,9 +17,10 @@ var boards = {};
17 17
  * @returns {A}
18 18
  */
19 19
 function noFail(fn) {
20
+  const monitored = monitorFunction(fn);
20 21
   return function noFailWrapped(arg) {
21 22
     try {
22
-      return fn(arg);
23
+      return monitored(arg);
23 24
     } catch (e) {
24 25
       console.trace(e);
25 26
     }
@@ -28,7 +29,7 @@ function noFail(fn) {
28 29
 
29 30
 function startIO(app) {
30 31
   io = iolib(app);
31
-  io.on("connection", noFail(socketConnection));
32
+  io.on("connection", noFail(handleSocketConnection));
32 33
   return io;
33 34
 }
34 35
 
@@ -41,6 +42,7 @@ function getBoard(name) {
41 42
   } else {
42 43
     var board = BoardData.load(name);
43 44
     boards[name] = board;
45
+    gauge("boards in memory", Object.keys(boards).length);
44 46
     return board;
45 47
   }
46 48
 }
@@ -49,7 +51,7 @@ function getBoard(name) {
49 51
  * Executes on every new connection
50 52
  * @param {iolib.Socket} socket
51 53
  */
52
-function socketConnection(socket) {
54
+function handleSocketConnection(socket) {
53 55
   /**
54 56
    * Function to call when an user joins a board
55 57
    * @param {string} name
@@ -64,6 +66,7 @@ function socketConnection(socket) {
64 66
     var board = await getBoard(name);
65 67
     board.users.add(socket.id);
66 68
     log("board joined", { board: board.name, users: board.users.size });
69
+    gauge("connected", board.users.size, {board: name});
67 70
     return board;
68 71
   }
69 72
 
@@ -141,9 +144,11 @@ function socketConnection(socket) {
141 144
         board.users.delete(socket.id);
142 145
         var userCount = board.users.size;
143 146
         log("disconnection", { board: board.name, users: board.users.size });
147
+        gauge("connected", userCount, { board: board.name });
144 148
         if (userCount === 0) {
145 149
           board.save();
146 150
           delete boards[room];
151
+          gauge("boards in memory", Object.keys(boards).length);
147 152
         }
148 153
       }
149 154
     });

Loading…
Annulla
Salva