diff --git a/.env.example b/.env.example index ed43d86..11b682f 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,7 @@ TLS=false TLS_KEY= TLS_CERT= JWT_SECRET_KEY=your_secret_key + +# Prometheus metrics endpoint +# Set this to access the monitoring endpoint at /metrics +# METRICS_TOKEN= diff --git a/package-lock.json b/package-lock.json index ea8cae1..b979909 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,10 +23,12 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "node-fetch": "^3.3.2", + "prom-client": "^14.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", + "socket.io-prometheus": "^0.3.0", "vue": "^2.7.14" }, "devDependencies": { @@ -3122,6 +3124,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", @@ -8418,6 +8426,18 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/prom-client": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", + "integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==", + "license": "Apache-2.0", + "dependencies": { + "tdigest": "^0.1.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9305,6 +9325,15 @@ "node": ">=10.0.0" } }, + "node_modules/socket.io-prometheus": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/socket.io-prometheus/-/socket.io-prometheus-0.3.0.tgz", + "integrity": "sha512-ERrHtTizVy/buZDMCAcrFD1WZJHM01B6t3OwkV+abQGCNj86+UlRLwvAfxLK+HP3/qqYrWNk9W3vywYYsRe09w==", + "license": "MIT", + "peerDependencies": { + "prom-client": ">=10 <15" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10028,6 +10057,15 @@ "node": ">=6" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 5379e01..9ffb3df 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "node-fetch": "^3.3.2", + "prom-client": "^14.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", + "socket.io-prometheus": "^0.3.0", "vue": "^2.7.14" }, "devDependencies": { diff --git a/websocket_server/app.js b/websocket_server/app.js index 9666b9f..9fda32a 100644 --- a/websocket_server/app.js +++ b/websocket_server/app.js @@ -4,6 +4,9 @@ */ import express from 'express' +import { register } from 'prom-client' + +const METRICS_TOKEN = process.env.METRICS_TOKEN const app = express() @@ -11,4 +14,14 @@ app.get('/', (req, res) => { res.send('Excalidraw collaboration server is up :)') }) +app.get('/metrics', async (req, res) => { + const token = req.headers.authorization?.split(' ')[1] || req.query.token + if (!METRICS_TOKEN || token !== METRICS_TOKEN) { + return res.status(403).send('Unauthorized') + } + const metrics = await register.metrics() + res.set('Content-Type', register.contentType) + res.end(metrics) +}) + export default app diff --git a/websocket_server/socket.js b/websocket_server/socket.js index a8f7664..8c831e0 100644 --- a/websocket_server/socket.js +++ b/websocket_server/socket.js @@ -6,6 +6,7 @@ */ import { Server as SocketIO } from 'socket.io' +import prometheusMetrics from 'socket.io-prometheus' import jwt from 'jsonwebtoken' import { getRoomDataFromFile, roomDataStore, saveRoomDataToFile } from './roomData.js' import { convertArrayBufferToString, convertStringToArrayBuffer } from './utils.js' @@ -42,6 +43,8 @@ export const initSocket = (server) => { io.use(socketAuthenticateHandler) + prometheusMetrics(io) + io.on('connection', (socket) => { setupSocketEvents(socket, io) })