From af6924a27c95124cf2b101ca1ea2704e5023fa32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 9 Jun 2020 11:49:23 +0200 Subject: [PATCH] Adding Prometheus metrics This commit adds a '/metrics' endpoint in the API that can be exploited by Prometheus. This endpoint returns: - the number of connected sockets - the number of users per room - common NodeJS and system metrics WARNING: this endpoint is public right now and should be protected --- back/package.json | 1 + back/src/App.ts | 5 ++++- back/src/Controller/IoSocketController.ts | 24 ++++++++++++++++++--- back/src/Controller/MapController.ts | 2 +- back/src/Controller/PrometheusController.ts | 20 +++++++++++++++++ back/yarn.lock | 19 ++++++++++++++++ 6 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 back/src/Controller/PrometheusController.ts diff --git a/back/package.json b/back/package.json index ca649707..fc5ef49a 100644 --- a/back/package.json +++ b/back/package.json @@ -32,6 +32,7 @@ "express": "^4.17.1", "http-status-codes": "^1.4.0", "jsonwebtoken": "^8.5.1", + "prom-client": "^12.0.0", "socket.io": "^2.3.0", "systeminformation": "^4.26.5", "ts-node-dev": "^1.0.0-pre.44", diff --git a/back/src/App.ts b/back/src/App.ts index 06e08ca6..e12afdb4 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -6,6 +6,7 @@ import {Application, Request, Response} from 'express'; import bodyParser = require('body-parser'); import * as http from "http"; import {MapController} from "./Controller/MapController"; +import {PrometheusController} from "./Controller/PrometheusController"; class App { public app: Application; @@ -13,6 +14,7 @@ class App { public ioSocketController: IoSocketController; public authenticateController: AuthenticateController; public mapController: MapController; + public prometheusController: PrometheusController; constructor() { this.app = express(); @@ -29,6 +31,7 @@ class App { this.ioSocketController = new IoSocketController(this.server); this.authenticateController = new AuthenticateController(this.app); this.mapController = new MapController(this.app); + this.prometheusController = new PrometheusController(this.app, this.ioSocketController); } // TODO add session user @@ -49,4 +52,4 @@ class App { } } -export default new App().server; \ No newline at end of file +export default new App().server; diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 8608f48b..9f8a5cad 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -12,6 +12,8 @@ import {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage" import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved"; import si from "systeminformation"; +import {Gauge} from "prom-client"; +import os from 'os'; enum SockerIoEvent { CONNECTION = "connection", @@ -31,12 +33,24 @@ enum SockerIoEvent { } export class IoSocketController { - Io: socketIO.Server; - Worlds: Map = new Map(); - sockets: Map = new Map(); + public readonly Io: socketIO.Server; + private Worlds: Map = new Map(); + private sockets: Map = new Map(); + private nbClientsGauge: Gauge; + private nbClientsPerRoomGauge: Gauge; constructor(server: http.Server) { this.Io = socketIO(server); + this.nbClientsGauge = new Gauge({ + name: 'workadventure_nb_sockets', + help: 'Number of connected sockets', + labelNames: [ 'host' ] + }); + this.nbClientsPerRoomGauge = new Gauge({ + name: 'workadventure_nb_clients_per_room', + help: 'Number of clients per room', + labelNames: [ 'host', 'room' ] + }); // Authentication with token. it will be decoded and stored in the socket. // Completely commented for now, as we do not use the "/login" route at all. @@ -103,6 +117,7 @@ export class IoSocketController { // Let's log server load when a user joins let srvSockets = this.Io.sockets.sockets; + this.nbClientsGauge.inc({ host: os.hostname() }); console.log(new Date().toISOString() + ' A user joined (', Object.keys(srvSockets).length, ' connected users)'); si.currentLoad().then(data => console.log(' Current load: ', data.avgload)); si.currentLoad().then(data => console.log(' CPU: ', data.currentload, '%')); @@ -231,6 +246,7 @@ export class IoSocketController { // Let's log server load when a user leaves let srvSockets = this.Io.sockets.sockets; + this.nbClientsGauge.dec({ host: os.hostname() }); console.log('A user left (', Object.keys(srvSockets).length, ' connected users)'); si.currentLoad().then(data => console.log('Current load: ', data.avgload)); si.currentLoad().then(data => console.log('CPU: ', data.currentload, '%')); @@ -267,6 +283,7 @@ export class IoSocketController { } //user leave previous room Client.leave(Client.roomId); + this.nbClientsPerRoomGauge.inc({ host: os.hostname(), room: Client.roomId }); delete Client.roomId; } } @@ -274,6 +291,7 @@ export class IoSocketController { private joinRoom(Client : ExSocketInterface, roomId: string, position: Point): World { //join user in room Client.join(roomId); + this.nbClientsPerRoomGauge.inc({ host: os.hostname(), room: roomId }); Client.roomId = roomId; Client.position = position; diff --git a/back/src/Controller/MapController.ts b/back/src/Controller/MapController.ts index 68243df5..e3730898 100644 --- a/back/src/Controller/MapController.ts +++ b/back/src/Controller/MapController.ts @@ -19,7 +19,7 @@ export class MapController { // Returns a map mapping map name to file name of the map getStartMap() { this.App.get("/start-map", (req: Request, res: Response) => { - return res.status(OK).send({ + res.status(OK).send({ mapUrlStart: req.headers.host + "/map/files" + URL_ROOM_STARTED, startInstance: "global" }); diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts new file mode 100644 index 00000000..0a0db2bb --- /dev/null +++ b/back/src/Controller/PrometheusController.ts @@ -0,0 +1,20 @@ +import {Application, Request, Response} from "express"; +import {IoSocketController} from "_Controller/IoSocketController"; +const register = require('prom-client').register; +const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; + +export class PrometheusController { + constructor(private App: Application, private ioSocketController: IoSocketController) { + collectDefaultMetrics({ + timeout: 10000, + gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets. + }); + + this.App.get("/metrics", this.metrics.bind(this)); + } + + private metrics(req: Request, res: Response): void { + res.set('Content-Type', register.contentType); + res.end(register.metrics()); + } +} diff --git a/back/yarn.lock b/back/yarn.lock index a37028af..28223723 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -266,6 +266,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -1333,6 +1338,13 @@ progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" +prom-client@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" + integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== + dependencies: + tdigest "^0.1.1" + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -1683,6 +1695,13 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"