mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Add README, implementation de l'api Docker sur le quizroom, Fonctionnel
remove docker group graph generator remake Docker API implementation
This commit is contained in:
parent
dabdfafd35
commit
fff5830afd
13 changed files with 740 additions and 415 deletions
|
|
@ -55,6 +55,8 @@ services:
|
|||
container_name: quizroom
|
||||
ports:
|
||||
- "4500:4500"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- quiz_network
|
||||
restart: always
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ services:
|
|||
- PORT=${PORT:-4500}
|
||||
ports:
|
||||
- "${PORT:-4500}:${PORT:-4500}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- PORT=${PORT:-4500}
|
||||
- ROOM_ID=${ROOM_ID}
|
||||
|
|
|
|||
319
quizRoom/package-lock.json
generated
319
quizRoom/package-lock.json
generated
|
|
@ -9,17 +9,24 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dockerode": "^4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"http": "^0.0.1-security",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "^3.3.32",
|
||||
"@types/express": "^5.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@balena/dockerignore": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
||||
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
|
|
@ -118,6 +125,27 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/docker-modem": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
|
||||
"integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/ssh2": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dockerode": {
|
||||
"version": "3.3.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz",
|
||||
"integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/docker-modem": "*",
|
||||
"@types/node": "*",
|
||||
"@types/ssh2": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
|
||||
|
|
@ -195,6 +223,30 @@
|
|||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz",
|
||||
"integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2/node_modules/@types/node": {
|
||||
"version": "18.19.67",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
|
||||
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
|
|
@ -243,6 +295,33 @@
|
|||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
|
|
@ -251,6 +330,24 @@
|
|||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
|
|
@ -290,6 +387,38 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
|
|
@ -318,6 +447,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
|
|
@ -365,6 +499,20 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
|
|
@ -432,6 +580,33 @@
|
|||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/docker-modem": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
|
||||
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"readable-stream": "^3.5.0",
|
||||
"split-ca": "^1.0.1",
|
||||
"ssh2": "^1.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
|
||||
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
|
||||
"dependencies": {
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"docker-modem": "^5.0.3",
|
||||
"tar-fs": "~2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
|
|
@ -458,6 +633,14 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
||||
|
|
@ -639,6 +822,11 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -760,6 +948,25 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
|
|
@ -839,11 +1046,22 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
|
||||
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
|
|
@ -884,6 +1102,14 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
|
@ -912,6 +1138,15 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
|
|
@ -951,6 +1186,19 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -1119,6 +1367,28 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split-ca": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
@ -1128,6 +1398,40 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
|
||||
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
|
@ -1180,6 +1484,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
|
@ -1220,6 +1529,11 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
|
@ -1243,6 +1557,11 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@
|
|||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "^3.3.32",
|
||||
"@types/express": "^5.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockerode": "^4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"http": "^0.0.1-security",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Server, Socket } from "socket.io";
|
||||
import os from "os";
|
||||
import Docker from 'dockerode';
|
||||
import fs from 'fs';
|
||||
|
||||
const MAX_USERS_PER_ROOM = 60;
|
||||
|
|
@ -113,171 +113,116 @@ export const setupWebsocket = (io: Server): void => {
|
|||
socket.to(roomName).emit("message-sent-student", { message });
|
||||
});
|
||||
|
||||
interface ContainerStats {
|
||||
containerId: string;
|
||||
containerName: string;
|
||||
memoryUsedMB: number | null;
|
||||
memoryUsedPercentage: number | null;
|
||||
cpuUsedPercentage: number | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class ContainerMetrics {
|
||||
private totalSystemMemory = os.totalmem();
|
||||
private cgroupv2 = this.isCgroupV2();
|
||||
private lastCPUUsage = 0;
|
||||
private lastCPUTime = Date.now();
|
||||
private docker: Docker;
|
||||
private containerName: string;
|
||||
|
||||
private isCgroupV2(): boolean {
|
||||
return fs.existsSync('/sys/fs/cgroup/cgroup.controllers');
|
||||
private bytesToMB(bytes: number): number {
|
||||
return Math.round(bytes / (1024 * 1024));
|
||||
}
|
||||
|
||||
private readCgroupFile(filepath: string): string {
|
||||
try {
|
||||
return fs.readFileSync(filepath, 'utf-8').trim();
|
||||
} catch (error) {
|
||||
console.debug(`Could not read ${filepath}`);
|
||||
return '';
|
||||
}
|
||||
constructor() {
|
||||
this.docker = new Docker({
|
||||
socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock'
|
||||
});
|
||||
this.containerName = `room_${process.env.ROOM_ID}`;
|
||||
}
|
||||
|
||||
private getCgroupCPUUsage(): number {
|
||||
try {
|
||||
if (this.cgroupv2) {
|
||||
const usage = this.readCgroupFile('/sys/fs/cgroup/cpu.stat');
|
||||
const usageMatch = usage.match(/usage_usec\s+(\d+)/);
|
||||
if (usageMatch) {
|
||||
const currentUsage = Number(usageMatch[1]) / 1000000;
|
||||
const currentTime = Date.now();
|
||||
const cpuDelta = currentUsage - this.lastCPUUsage;
|
||||
const timeDelta = (currentTime - this.lastCPUTime) / 1000;
|
||||
|
||||
this.lastCPUUsage = currentUsage;
|
||||
this.lastCPUTime = currentTime;
|
||||
|
||||
return (cpuDelta / timeDelta) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
const cgroupV1Paths = [
|
||||
'/sys/fs/cgroup/cpu/cpuacct.usage',
|
||||
'/sys/fs/cgroup/cpuacct/cpuacct.usage',
|
||||
'/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage'
|
||||
];
|
||||
|
||||
for (const path of cgroupV1Paths) {
|
||||
const usage = this.readCgroupFile(path);
|
||||
if (usage) {
|
||||
const currentUsage = Number(usage) / 1000000000;
|
||||
const currentTime = Date.now();
|
||||
const cpuDelta = currentUsage - this.lastCPUUsage;
|
||||
const timeDelta = (currentTime - this.lastCPUTime) / 1000;
|
||||
|
||||
this.lastCPUUsage = currentUsage;
|
||||
this.lastCPUTime = currentTime;
|
||||
|
||||
return (cpuDelta / timeDelta) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getFallbackCPUUsage();
|
||||
} catch (error) {
|
||||
return this.getFallbackCPUUsage();
|
||||
}
|
||||
private async getContainerNetworks(containerId: string): Promise<string[]> {
|
||||
const container = this.docker.getContainer(containerId);
|
||||
const info = await container.inspect();
|
||||
return Object.keys(info.NetworkSettings.Networks);
|
||||
}
|
||||
|
||||
private getFallbackCPUUsage(): number {
|
||||
public async getAllContainerStats(): Promise<ContainerStats[]> {
|
||||
try {
|
||||
const cpus = os.cpus();
|
||||
return cpus.reduce((acc, cpu) => {
|
||||
const total = Object.values(cpu.times).reduce((a, b) => a + b);
|
||||
const idle = cpu.times.idle;
|
||||
return acc + ((total - idle) / total) * 100;
|
||||
}, 0) / cpus.length;
|
||||
} catch (error) {
|
||||
console.error('Error getting fallback CPU usage:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// First get our container to find its networks
|
||||
const ourContainer = await this.docker.listContainers({
|
||||
all: true,
|
||||
filters: { name: [this.containerName] }
|
||||
});
|
||||
|
||||
private getCgroupMemoryUsage(): { used: number; limit: number } | null {
|
||||
try {
|
||||
// First get process memory as baseline
|
||||
const processMemory = process.memoryUsage();
|
||||
const baselineMemory = processMemory.rss;
|
||||
|
||||
if (this.cgroupv2) {
|
||||
const memUsage = Number(this.readCgroupFile('/sys/fs/cgroup/memory.current'));
|
||||
if (!isNaN(memUsage) && memUsage > 0) {
|
||||
return {
|
||||
used: Math.max(baselineMemory, memUsage),
|
||||
limit: this.totalSystemMemory
|
||||
};
|
||||
}
|
||||
if (!ourContainer.length) {
|
||||
throw new Error(`Container ${this.containerName} not found`);
|
||||
}
|
||||
|
||||
// Try cgroup v1
|
||||
const v1Paths = {
|
||||
usage: '/sys/fs/cgroup/memory/memory.usage_in_bytes',
|
||||
limit: '/sys/fs/cgroup/memory/memory.limit_in_bytes'
|
||||
};
|
||||
const ourNetworks = await this.getContainerNetworks(ourContainer[0].Id);
|
||||
|
||||
const memoryUsage = Number(this.readCgroupFile(v1Paths.usage));
|
||||
if (!isNaN(memoryUsage) && memoryUsage > 0) {
|
||||
return {
|
||||
used: Math.max(baselineMemory, memoryUsage),
|
||||
limit: this.totalSystemMemory
|
||||
};
|
||||
}
|
||||
// Get all containers
|
||||
const allContainers = await this.docker.listContainers();
|
||||
|
||||
// Fallback to process memory
|
||||
return {
|
||||
used: baselineMemory,
|
||||
limit: this.totalSystemMemory
|
||||
};
|
||||
// Get stats for containers on the same networks
|
||||
const containerStats = await Promise.all(
|
||||
allContainers.map(async (container): Promise<ContainerStats | null> => {
|
||||
try {
|
||||
const containerNetworks = await this.getContainerNetworks(container.Id);
|
||||
// Check if container shares any network with our container
|
||||
if (!containerNetworks.some(network => ourNetworks.includes(network))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = await this.docker.getContainer(container.Id).stats({ stream: false });
|
||||
|
||||
const memoryStats = {
|
||||
usage: stats.memory_stats.usage,
|
||||
limit: stats.memory_stats.limit || 0,
|
||||
percent: stats.memory_stats.limit ? (stats.memory_stats.usage / stats.memory_stats.limit) * 100 : 0
|
||||
};
|
||||
|
||||
const cpuDelta = stats.cpu_stats?.cpu_usage?.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage || 0);
|
||||
const systemDelta = stats.cpu_stats?.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage || 0);
|
||||
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * (stats.cpu_stats?.online_cpus || 1) * 100 : 0;
|
||||
|
||||
return {
|
||||
containerId: container.Id,
|
||||
containerName: container.Names[0].replace(/^\//, ''),
|
||||
memoryUsedMB: this.bytesToMB(memoryStats.usage),
|
||||
memoryUsedPercentage: memoryStats.percent,
|
||||
cpuUsedPercentage: cpuPercent
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
containerId: container.Id,
|
||||
containerName: container.Names[0].replace(/^\//, ''),
|
||||
memoryUsedMB: null,
|
||||
memoryUsedPercentage: null,
|
||||
cpuUsedPercentage: null,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Change the filter to use proper type predicate
|
||||
return containerStats.filter((stats): stats is ContainerStats => stats !== null);
|
||||
} catch (error) {
|
||||
console.debug('Error reading cgroup memory:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public getMetrics() {
|
||||
try {
|
||||
const mbFactor = 1024 * 1024;
|
||||
let memoryData = this.getCgroupMemoryUsage();
|
||||
|
||||
if (!memoryData) {
|
||||
const processMemory = process.memoryUsage();
|
||||
memoryData = {
|
||||
used: processMemory.rss,
|
||||
limit: this.totalSystemMemory
|
||||
};
|
||||
}
|
||||
|
||||
const memoryUsedMB = memoryData.used / mbFactor;
|
||||
const memoryTotalMB = memoryData.limit / mbFactor;
|
||||
const memoryPercentage = (memoryData.used / memoryData.limit) * 100;
|
||||
|
||||
console.debug(`
|
||||
Memory Usage: ${memoryUsedMB.toFixed(2)} MB
|
||||
Memory Total: ${memoryTotalMB.toFixed(2)} MB
|
||||
Memory %: ${memoryPercentage.toFixed(2)}%
|
||||
`);
|
||||
|
||||
return {
|
||||
memoryUsedMB: memoryUsedMB.toFixed(2),
|
||||
memoryUsedPercentage: memoryPercentage.toFixed(2),
|
||||
cpuUsedPercentage: this.getCgroupCPUUsage().toFixed(2)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting metrics:", error);
|
||||
return {
|
||||
memoryUsedMB: "0",
|
||||
memoryUsedPercentage: "0",
|
||||
cpuUsedPercentage: "0"
|
||||
};
|
||||
console.error('Stats error:', error);
|
||||
return [{
|
||||
containerId: 'unknown',
|
||||
containerName: 'unknown',
|
||||
memoryUsedMB: null,
|
||||
memoryUsedPercentage: null,
|
||||
cpuUsedPercentage: null,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in WebSocket setup
|
||||
const containerMetrics = new ContainerMetrics();
|
||||
|
||||
socket.on("get-usage", () => {
|
||||
socket.on("get-usage", async () => {
|
||||
try {
|
||||
const usageData = containerMetrics.getMetrics();
|
||||
const usageData = await containerMetrics.getAllContainerStats();
|
||||
socket.emit("usage-data", usageData);
|
||||
} catch (error) {
|
||||
socket.emit("error", { message: "Failed to retrieve usage data" });
|
||||
|
|
|
|||
|
|
@ -99,7 +99,10 @@ class DockerRoomProvider extends BaseRoomProvider {
|
|||
NetworkMode: this.docker_network,
|
||||
RestartPolicy: {
|
||||
Name: 'unless-stopped'
|
||||
}
|
||||
},
|
||||
Binds: [
|
||||
'/var/run/docker.sock:/var/run/docker.sock'
|
||||
]
|
||||
},
|
||||
Env: [
|
||||
`ROOM_ID=${roomId}`,
|
||||
|
|
|
|||
1
test/stressTest/.dockerignore
Normal file
1
test/stressTest/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
BASE_URL=http://host.docker.internal #via Docker
|
||||
BASE_URL=http://localhost # Via npm
|
||||
# Target url
|
||||
BASE_URL=http://msevignyl.duckdns.org
|
||||
|
||||
# Connection account
|
||||
USER_EMAIL=admin@admin.com
|
||||
USER_PASSWORD=admin
|
||||
|
||||
# Stress test parameters
|
||||
NUMBER_ROOMS=5
|
||||
USERS_PER_ROOM=60
|
||||
MAX_MESSAGES=20
|
||||
|
||||
# Optionnal
|
||||
|
||||
|
||||
MAX_MESSAGES_ROUND=20
|
||||
CONVERSATION_INTERVAL=1000
|
||||
MESSAGE_RESPONSE_TIMEOUT=5000
|
||||
BATCH_DELAY=1000
|
||||
BATCH_SIZE=10
|
||||
51
test/stressTest/README.md
Normal file
51
test/stressTest/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Test de Charge - EvalueTonSavoir
|
||||
|
||||
Ce conteneur permet d'exécuter des tests de charge sur l'application EvalueTonSavoir.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Créez un fichier `.env` à partir du modèle `.env.example`:
|
||||
|
||||
```bash
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
2. Modifiez les variables dans le fichier .env:
|
||||
|
||||
```bash
|
||||
# URL de l'application cible
|
||||
BASE_URL=http://votre-url.com
|
||||
|
||||
# Compte de connexion
|
||||
USER_EMAIL=admin@admin.com
|
||||
USER_PASSWORD=admin
|
||||
|
||||
# Paramètres du test de charge
|
||||
NUMBER_ROOMS=5 # Nombre de salles à créer
|
||||
USERS_PER_ROOM=60 # Nombre d'utilisateurs par salle
|
||||
|
||||
```
|
||||
#### Paramètres optionnels
|
||||
Dans le fichier .env, vous pouvez aussi configurer:
|
||||
|
||||
```bash
|
||||
MAX_MESSAGES_ROUND=20 # Messages maximum par cycle
|
||||
CONVERSATION_INTERVAL=1000 # Intervalle entre les messages (ms)
|
||||
MESSAGE_RESPONSE_TIMEOUT=5000 # Timeout des réponses (ms)
|
||||
BATCH_DELAY=1000 # Délai entre les lots (ms)
|
||||
BATCH_SIZE=10 # Taille des lots d'utilisateurs
|
||||
```
|
||||
|
||||
## Démarrage
|
||||
Pour lancer le test de charge:
|
||||
|
||||
Les résultats seront disponibles dans le dossier output/.
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
|
@ -22,18 +22,34 @@ export class Watcher extends RoomParticipant {
|
|||
try {
|
||||
this.socket.emit("get-usage");
|
||||
this.socket.once("usage-data", (data) => {
|
||||
//console.log(`Watcher ${this.username} received data:`, data);
|
||||
this.roomRessourcesData.push({ timestamp: Date.now(), ...data });
|
||||
const timestamp = Date.now();
|
||||
// Store each container's metrics separately with timestamp
|
||||
data.forEach(containerStat => {
|
||||
const existingData = this.roomRessourcesData.find(d => d.containerId === containerStat.containerId);
|
||||
if (existingData) {
|
||||
existingData.metrics.push({
|
||||
timestamp,
|
||||
...containerStat
|
||||
});
|
||||
} else {
|
||||
this.roomRessourcesData.push({
|
||||
containerId: containerStat.containerId,
|
||||
containerName: containerStat.containerName,
|
||||
metrics: [{
|
||||
timestamp,
|
||||
...containerStat
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Error capturing metrics for room ${this.roomName}:`, error.message);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Socket not connected for room ${this.roomName}`);
|
||||
}
|
||||
}
|
||||
|
||||
startCheckingResources(intervalMs = 250) {
|
||||
startCheckingResources(intervalMs = 500) {
|
||||
if (this.checkRessourceInterval) {
|
||||
console.warn(`Resource checking is already running for room ${this.roomName}.`);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -7,16 +7,10 @@ services:
|
|||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: stress-test
|
||||
networks:
|
||||
- quiz_network
|
||||
network_mode: host
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "9229:9229"
|
||||
volumes:
|
||||
- ./output:/app/output
|
||||
tty: true
|
||||
stdin_open: true
|
||||
#command: node --inspect=0.0.0.0:9229 main.js
|
||||
|
||||
networks:
|
||||
quiz_network:
|
||||
driver: bridge
|
||||
|
|
@ -15,21 +15,22 @@ const config = {
|
|||
password: process.env.USER_PASSWORD || 'admin'
|
||||
},
|
||||
rooms: {
|
||||
count: parseInt(process.env.NUMBER_ROOMS || '2'),
|
||||
count: parseInt(process.env.NUMBER_ROOMS || '15'),
|
||||
usersPerRoom: parseInt(process.env.USERS_PER_ROOM || '60'),
|
||||
batchSize: 5,
|
||||
batchDelay: 250
|
||||
batchSize: parseInt(process.env.BATCH_SIZE || 5),
|
||||
batchDelay: parseInt(process.env.BATCH_DELAY || 250)
|
||||
},
|
||||
simulation: {
|
||||
maxMessages: parseInt(process.env.MAX_MESSAGES || '20'),
|
||||
maxMessages: parseInt(process.env.MAX_MESSAGES_ROUND || '20'),
|
||||
messageInterval: parseInt(process.env.CONVERSATION_INTERVAL || '1000'),
|
||||
responseTimeout: 5000
|
||||
responseTimeout: parseInt(process.env.MESSAGE_RESPONSE_TIMEOUT || 5000)
|
||||
}
|
||||
};
|
||||
|
||||
const rooms = new Map();
|
||||
const metrics = new TestMetrics();
|
||||
|
||||
// Changes to setupRoom function
|
||||
async function setupRoom(token, index) {
|
||||
try {
|
||||
const room = await createRoomContainer(config.baseUrl, token);
|
||||
|
|
@ -37,7 +38,8 @@ async function setupRoom(token, index) {
|
|||
metrics.roomsCreated++;
|
||||
|
||||
const teacher = new Teacher(`teacher_${index}`, room.id);
|
||||
const watcher = new Watcher(`watcher_${index}`, room.id);
|
||||
// Only create watcher for first room (index 0)
|
||||
const watcher = index === 0 ? new Watcher(`watcher_${index}`, room.id) : null;
|
||||
|
||||
await Promise.all([
|
||||
teacher.connectToRoom(config.baseUrl)
|
||||
|
|
@ -47,16 +49,24 @@ async function setupRoom(token, index) {
|
|||
metrics.logError('teacherConnection', err);
|
||||
console.warn(`Teacher ${index} connection failed:`, err.message);
|
||||
}),
|
||||
watcher.connectToRoom(config.baseUrl)
|
||||
.then(() => metrics.usersConnected++)
|
||||
.catch(err => {
|
||||
metrics.userConnectionsFailed++;
|
||||
metrics.logError('watcherConnection', err);
|
||||
console.warn(`Watcher ${index} connection failed:`, err.message);
|
||||
})
|
||||
// Only connect watcher if it exists
|
||||
...(watcher ? [
|
||||
watcher.connectToRoom(config.baseUrl)
|
||||
.then(() => metrics.usersConnected++)
|
||||
.catch(err => {
|
||||
metrics.userConnectionsFailed++;
|
||||
metrics.logError('watcherConnection', err);
|
||||
console.warn(`Watcher ${index} connection failed:`, err.message);
|
||||
})
|
||||
] : [])
|
||||
]);
|
||||
|
||||
const students = Array.from({ length: config.rooms.usersPerRoom - 2 },
|
||||
// Adjust number of students based on whether room has a watcher
|
||||
const studentCount = watcher ?
|
||||
config.rooms.usersPerRoom - 2 : // Room with watcher: subtract teacher and watcher
|
||||
config.rooms.usersPerRoom - 1; // Rooms without watcher: subtract only teacher
|
||||
|
||||
const students = Array.from({ length: studentCount },
|
||||
(_, i) => new Student(`student_${index}_${i}`, room.id));
|
||||
|
||||
rooms.set(room.id, { teacher, watcher, students });
|
||||
|
|
@ -133,13 +143,14 @@ async function simulate() {
|
|||
}
|
||||
|
||||
async function generateReport() {
|
||||
const data = Object.fromEntries(
|
||||
Array.from(rooms.entries()).map(([id, { watcher }]) => [
|
||||
id,
|
||||
watcher.roomRessourcesData
|
||||
])
|
||||
);
|
||||
return generateMetricsReport(data,metrics);
|
||||
const watcherRoom = Array.from(rooms.entries()).find(([_, room]) => room.watcher);
|
||||
if (!watcherRoom) {
|
||||
throw new Error('No watcher found in any room');
|
||||
}
|
||||
const data = {
|
||||
[watcherRoom[0]]: watcherRoom[1].watcher.roomRessourcesData
|
||||
};
|
||||
return generateMetricsReport(data, metrics);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
|
|
@ -178,6 +189,8 @@ async function main() {
|
|||
} catch (error) {
|
||||
metrics.logError('main', error);
|
||||
console.error('Error:', error.message);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,126 +2,6 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
|
||||
|
||||
function ensureDirectoryExists(directory) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChart(chartJSNodeCanvas, data, xLabel, yLabel, outputFile, title) {
|
||||
const chartConfig = {
|
||||
type: 'line',
|
||||
data,
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
title: { display: true, text: xLabel }
|
||||
},
|
||||
y: {
|
||||
title: { display: true, text: yLabel }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buffer = await chartJSNodeCanvas.renderToBuffer(chartConfig);
|
||||
fs.writeFileSync(outputFile, buffer);
|
||||
}
|
||||
|
||||
async function generateRoomGraphs(roomId, validRoomData, chartJSNodeCanvas, roomDir) {
|
||||
const timeLabels = validRoomData.map(d => new Date(parseInt(d.timestamp)).toLocaleTimeString());
|
||||
|
||||
await Promise.all([
|
||||
// Room Memory Usage (MB)
|
||||
saveChart(chartJSNodeCanvas, {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: `Room ${roomId} Memory (MB)`,
|
||||
data: validRoomData.map(d => parseFloat(d.memoryUsedMB || 0)),
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}, 'Time', 'Memory Usage (MB)', path.join(roomDir, 'memory-usage-mb.png'),
|
||||
`Room ${roomId} Memory Usage in MB`),
|
||||
|
||||
// Room Memory Usage (Percentage)
|
||||
saveChart(chartJSNodeCanvas, {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: `Room ${roomId} Memory %`,
|
||||
data: validRoomData.map(d => parseFloat(d.memoryUsedPercentage || 0)),
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}, 'Time', 'Memory Usage %', path.join(roomDir, 'memory-usage-percent.png'),
|
||||
`Room ${roomId} Memory Usage Percentage`),
|
||||
|
||||
// Room CPU Usage
|
||||
saveChart(chartJSNodeCanvas, {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: `Room ${roomId} CPU Usage %`,
|
||||
data: validRoomData.map(d => parseFloat(d.cpuUsedPercentage || 0)),
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}, 'Time', 'CPU Usage %', path.join(roomDir, 'cpu-usage.png'),
|
||||
`Room ${roomId} CPU Impact`)
|
||||
]);
|
||||
}
|
||||
|
||||
async function generateGlobalGraphs(data, chartJSNodeCanvas, globalMetricsDir) {
|
||||
await Promise.all([
|
||||
saveChart(chartJSNodeCanvas, {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Total System Memory Used (MB)',
|
||||
data: data.memoryMB,
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}, 'Time', 'Total Memory Usage (MB)', path.join(globalMetricsDir, 'total-system-memory-mb.png')),
|
||||
|
||||
saveChart(chartJSNodeCanvas, {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Total System Memory Used %',
|
||||
data: data.memoryPercentage,
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}, 'Time', 'Total Memory Usage %', path.join(globalMetricsDir, 'total-system-memory-percent.png')),
|
||||
|
||||
saveChart(chartJSNodeCanvas, {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Total System CPU Usage %',
|
||||
data: data.cpuPercentage,
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
}, 'Time', 'Total CPU Usage %', path.join(globalMetricsDir, 'total-system-cpu.png'))
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveMetricsSummary(metrics, baseOutputDir) {
|
||||
const metricsData = metrics.getSummary();
|
||||
|
||||
|
|
@ -167,121 +47,207 @@ ${Object.entries(metricsData.errors)
|
|||
);
|
||||
}
|
||||
|
||||
export default async function generateMetricsReport(allRoomsData, testMetrics) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const baseOutputDir = `./output/${timestamp}`;
|
||||
ensureDirectoryExists(baseOutputDir);
|
||||
|
||||
if (testMetrics) {
|
||||
await saveMetricsSummary(testMetrics, baseOutputDir);
|
||||
}
|
||||
|
||||
const globalMetricsDir = path.join(baseOutputDir, 'global');
|
||||
ensureDirectoryExists(globalMetricsDir);
|
||||
|
||||
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: 800, height: 400 });
|
||||
|
||||
// Process individual room graphs first
|
||||
for (const [roomId, roomData] of Object.entries(allRoomsData)) {
|
||||
if (!Array.isArray(roomData)) {
|
||||
console.warn(`Invalid data format for room ${roomId}`);
|
||||
continue;
|
||||
// Common chart configurations
|
||||
const CHART_CONFIG = {
|
||||
width: 800,
|
||||
height: 400,
|
||||
chartStyles: {
|
||||
memory: {
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)'
|
||||
},
|
||||
memoryPercent: {
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)'
|
||||
},
|
||||
cpu: {
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)'
|
||||
}
|
||||
|
||||
const roomDir = path.join(baseOutputDir, `room_${roomId}`);
|
||||
ensureDirectoryExists(roomDir);
|
||||
|
||||
const validRoomData = roomData.filter(d => {
|
||||
const isValid = d && d.timestamp &&
|
||||
typeof d.memoryUsedMB !== 'undefined' &&
|
||||
typeof d.memoryUsedPercentage !== 'undefined' &&
|
||||
typeof d.cpuUsedPercentage !== 'undefined';
|
||||
if (!isValid) {
|
||||
console.warn(`Invalid metric data in room ${roomId}:`, d);
|
||||
}
|
||||
return isValid;
|
||||
});
|
||||
|
||||
if (validRoomData.length === 0) {
|
||||
console.warn(`No valid data for room ${roomId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await generateRoomGraphs(roomId, validRoomData, chartJSNodeCanvas, roomDir);
|
||||
}
|
||||
};
|
||||
|
||||
const createBaseChartConfig = (labels, dataset, xLabel, yLabel) => ({
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [dataset]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
x: { title: { display: true, text: xLabel }},
|
||||
y: { title: { display: true, text: yLabel }}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: true, position: 'top' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Process global metrics with time-based averaging
|
||||
const timeWindows = {};
|
||||
const timeInterval = 1000; // 250ms windows
|
||||
const totalRooms = Object.keys(allRoomsData).length;
|
||||
|
||||
// Group data into time windows
|
||||
Object.entries(allRoomsData).forEach(([roomId, roomData]) => {
|
||||
if (!Array.isArray(roomData)) return;
|
||||
|
||||
roomData.forEach(metric => {
|
||||
if (!metric?.timestamp) return;
|
||||
|
||||
const timeWindow = Math.floor(parseInt(metric.timestamp) / timeInterval) * timeInterval;
|
||||
|
||||
if (!timeWindows[timeWindow]) {
|
||||
timeWindows[timeWindow] = {
|
||||
rooms: new Map(),
|
||||
roomCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (!timeWindows[timeWindow].rooms.has(roomId)) {
|
||||
timeWindows[timeWindow].rooms.set(roomId, {
|
||||
memoryMB: [],
|
||||
memoryPercentage: [],
|
||||
cpuPercentage: []
|
||||
});
|
||||
timeWindows[timeWindow].roomCount++;
|
||||
}
|
||||
|
||||
const roomMetrics = timeWindows[timeWindow].rooms.get(roomId);
|
||||
roomMetrics.memoryMB.push(parseFloat(metric.memoryUsedMB || 0));
|
||||
roomMetrics.memoryPercentage.push(parseFloat(metric.memoryUsedPercentage || 0));
|
||||
roomMetrics.cpuPercentage.push(parseFloat(metric.cpuUsedPercentage || 0));
|
||||
});
|
||||
});
|
||||
|
||||
// Process only windows with data from all rooms
|
||||
const globalMetrics = Object.entries(timeWindows)
|
||||
.filter(([_, data]) => data.roomCount === totalRooms) // Only windows with all rooms
|
||||
.map(([timestamp, data]) => {
|
||||
const totals = Array.from(data.rooms.values()).reduce((acc, room) => {
|
||||
// Calculate room averages
|
||||
const memoryMBAvg = room.memoryMB.reduce((a, b) => a + b, 0) / room.memoryMB.length;
|
||||
const memoryPercentageAvg = room.memoryPercentage.reduce((a, b) => a + b, 0) / room.memoryPercentage.length;
|
||||
const cpuPercentageAvg = room.cpuPercentage.reduce((a, b) => a + b, 0) / room.cpuPercentage.length;
|
||||
|
||||
// Sum room averages
|
||||
return {
|
||||
memoryMB: acc.memoryMB + memoryMBAvg,
|
||||
memoryPercentage: acc.memoryPercentage + memoryPercentageAvg,
|
||||
cpuPercentage: acc.cpuPercentage + cpuPercentageAvg
|
||||
};
|
||||
}, { memoryMB: 0, memoryPercentage: 0, cpuPercentage: 0 });
|
||||
|
||||
return {
|
||||
timestamp: parseInt(timestamp),
|
||||
...totals
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
// Generate global graphs with complete window data
|
||||
const timeLabels = globalMetrics.map(d => new Date(d.timestamp).toLocaleTimeString());
|
||||
await generateGlobalGraphs({
|
||||
labels: timeLabels,
|
||||
memoryMB: globalMetrics.map(d => d.memoryMB),
|
||||
memoryPercentage: globalMetrics.map(d => d.memoryPercentage),
|
||||
cpuPercentage: globalMetrics.map(d => d.cpuPercentage)
|
||||
}, chartJSNodeCanvas, globalMetricsDir);
|
||||
|
||||
return { outputDir: baseOutputDir };
|
||||
function ensureDirectoryExists(directory) {
|
||||
!fs.existsSync(directory) && fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
|
||||
async function generateMetricsChart(chartJSNodeCanvas, data, style, label, timeLabels, metric, outputPath) {
|
||||
const dataset = {
|
||||
label,
|
||||
data: data.map(m => m[metric] || 0),
|
||||
...CHART_CONFIG.chartStyles[style],
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
};
|
||||
|
||||
const buffer = await chartJSNodeCanvas.renderToBuffer(
|
||||
createBaseChartConfig(timeLabels, dataset, 'Time', label)
|
||||
);
|
||||
|
||||
return fs.promises.writeFile(outputPath, buffer);
|
||||
}
|
||||
|
||||
async function generateContainerCharts(chartJSNodeCanvas, containerData, outputDir) {
|
||||
const timeLabels = containerData.metrics.map(m =>
|
||||
new Date(m.timestamp).toLocaleTimeString()
|
||||
);
|
||||
|
||||
const chartPromises = [
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
containerData.metrics,
|
||||
'memory',
|
||||
`${containerData.containerName} Memory (MB)`,
|
||||
timeLabels,
|
||||
'memoryUsedMB',
|
||||
path.join(outputDir, 'memory-usage-mb.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
containerData.metrics,
|
||||
'memoryPercent',
|
||||
`${containerData.containerName} Memory %`,
|
||||
timeLabels,
|
||||
'memoryUsedPercentage',
|
||||
path.join(outputDir, 'memory-usage-percent.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
containerData.metrics,
|
||||
'cpu',
|
||||
`${containerData.containerName} CPU %`,
|
||||
timeLabels,
|
||||
'cpuUsedPercentage',
|
||||
path.join(outputDir, 'cpu-usage.png')
|
||||
)
|
||||
];
|
||||
|
||||
await Promise.all(chartPromises);
|
||||
}
|
||||
|
||||
async function processSummaryMetrics(containers, timeLabels) {
|
||||
return timeLabels.map((_, timeIndex) => ({
|
||||
memoryUsedMB: containers.reduce((sum, container) =>
|
||||
sum + (container.metrics[timeIndex]?.memoryUsedMB || 0), 0),
|
||||
memoryUsedPercentage: containers.reduce((sum, container) =>
|
||||
sum + (container.metrics[timeIndex]?.memoryUsedPercentage || 0), 0),
|
||||
cpuUsedPercentage: containers.reduce((sum, container) =>
|
||||
sum + (container.metrics[timeIndex]?.cpuUsedPercentage || 0), 0)
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function generateMetricsReport(allRoomsData, testMetrics) {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const baseOutputDir = `./output/${timestamp}`;
|
||||
const dirs = [
|
||||
baseOutputDir,
|
||||
path.join(baseOutputDir, 'all-containers'),
|
||||
path.join(baseOutputDir, 'all-rooms')
|
||||
];
|
||||
|
||||
dirs.forEach(ensureDirectoryExists);
|
||||
|
||||
if (testMetrics) {
|
||||
await saveMetricsSummary(testMetrics, baseOutputDir);
|
||||
}
|
||||
|
||||
const chartJSNodeCanvas = new ChartJSNodeCanvas(CHART_CONFIG);
|
||||
const allContainers = Object.values(allRoomsData).flat();
|
||||
const roomContainers = allContainers.filter(c =>
|
||||
c.containerName.startsWith('room_')
|
||||
);
|
||||
|
||||
if (allContainers.length > 0) {
|
||||
const timeLabels = allContainers[0].metrics.map(m =>
|
||||
new Date(m.timestamp).toLocaleTimeString()
|
||||
);
|
||||
const summedMetrics = await processSummaryMetrics(allContainers, timeLabels);
|
||||
await generateContainerSummaryCharts(
|
||||
chartJSNodeCanvas,
|
||||
summedMetrics,
|
||||
timeLabels,
|
||||
path.join(baseOutputDir, 'all-containers')
|
||||
);
|
||||
}
|
||||
|
||||
if (roomContainers.length > 0) {
|
||||
const timeLabels = roomContainers[0].metrics.map(m =>
|
||||
new Date(m.timestamp).toLocaleTimeString()
|
||||
);
|
||||
const summedMetrics = await processSummaryMetrics(roomContainers, timeLabels);
|
||||
await generateContainerSummaryCharts(
|
||||
chartJSNodeCanvas,
|
||||
summedMetrics,
|
||||
timeLabels,
|
||||
path.join(baseOutputDir, 'all-rooms')
|
||||
);
|
||||
}
|
||||
|
||||
// Process individual containers
|
||||
const containerPromises = Object.values(allRoomsData)
|
||||
.flat()
|
||||
.filter(container => !container.containerName.startsWith('room_'))
|
||||
.map(async containerData => {
|
||||
const containerDir = path.join(baseOutputDir, containerData.containerName);
|
||||
ensureDirectoryExists(containerDir);
|
||||
await generateContainerCharts(chartJSNodeCanvas, containerData, containerDir);
|
||||
});
|
||||
|
||||
await Promise.all(containerPromises);
|
||||
|
||||
return { outputDir: baseOutputDir };
|
||||
} catch (error) {
|
||||
console.error('Error generating metrics report:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateContainerSummaryCharts(chartJSNodeCanvas, metrics, timeLabels, outputDir) {
|
||||
await Promise.all([
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
metrics,
|
||||
'memory',
|
||||
'Total Memory (MB)',
|
||||
timeLabels,
|
||||
'memoryUsedMB',
|
||||
path.join(outputDir, 'total-memory-usage-mb.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
metrics,
|
||||
'memoryPercent',
|
||||
'Total Memory %',
|
||||
timeLabels,
|
||||
'memoryUsedPercentage',
|
||||
path.join(outputDir, 'total-memory-usage-percent.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
metrics,
|
||||
'cpu',
|
||||
'Total CPU %',
|
||||
timeLabels,
|
||||
'cpuUsedPercentage',
|
||||
path.join(outputDir, 'total-cpu-usage.png')
|
||||
)
|
||||
]);
|
||||
}
|
||||
Loading…
Reference in a new issue