feat: add production mode
There are two ways to enable this production mode:
- either with the new "mode" option:
```js
instrument(io, {
mode: "production" // defaults to "development"
});
```
- or with the NODE_ENV environment variable:
```
NODE_ENV=production node index.js
```
In production mode, the server won't send all details about the socket
instances and the rooms, thus reducing the memory footprint of the
instrumentation.
Related:
- https://github.com/socketio/socket.io-admin-ui/issues/22
- https://github.com/socketio/socket.io-admin-ui/issues/23
This commit is contained in:
parent
481ef22b3a
commit
e0d91cadb1
124
lib/index.ts
124
lib/index.ts
|
|
@ -2,6 +2,8 @@ import { Namespace, RemoteSocket, Server, Socket } from "socket.io";
|
|||
import {
|
||||
ClientEvents,
|
||||
Feature,
|
||||
NamespaceDetails,
|
||||
NamespaceEvent,
|
||||
SerializedSocket,
|
||||
ServerEvents,
|
||||
} from "./typed-events";
|
||||
|
|
@ -46,6 +48,10 @@ interface InstrumentOptions {
|
|||
* The store
|
||||
*/
|
||||
store: Store;
|
||||
/**
|
||||
* Whether to send all events or only aggregated events to the UI, for performance purposes.
|
||||
*/
|
||||
mode: "development" | "production";
|
||||
}
|
||||
|
||||
const initAuthenticationMiddleware = (
|
||||
|
|
@ -120,16 +126,26 @@ const initStatsEmitter = (
|
|||
pid: process.pid,
|
||||
};
|
||||
|
||||
const io = adminNamespace.server;
|
||||
|
||||
const emitStats = () => {
|
||||
debug("emit stats");
|
||||
// @ts-ignore private reference
|
||||
const clientsCount = adminNamespace.server.engine.clientsCount;
|
||||
const namespaces: NamespaceDetails[] = [];
|
||||
io._nsps.forEach((namespace) => {
|
||||
namespaces.push({
|
||||
name: namespace.name,
|
||||
socketsCount: namespace.sockets.size,
|
||||
});
|
||||
});
|
||||
|
||||
adminNamespace.emit(
|
||||
"server_stats",
|
||||
Object.assign({}, baseStats, {
|
||||
uptime: process.uptime(),
|
||||
clientsCount,
|
||||
clientsCount: io.engine.clientsCount,
|
||||
pollingClientsCount: io._pollingClientsCount,
|
||||
aggregatedEvents: io._eventBuffer.getValuesAndClear(),
|
||||
namespaces,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
@ -295,7 +311,7 @@ const registerFeatureHandlers = (
|
|||
}
|
||||
};
|
||||
|
||||
const registerListeners = (
|
||||
const registerVerboseListeners = (
|
||||
adminNamespace: Namespace<{}, ServerEvents>,
|
||||
nsp: Namespace
|
||||
) => {
|
||||
|
|
@ -407,6 +423,81 @@ const serializeData = (data: any) => {
|
|||
return obj;
|
||||
};
|
||||
|
||||
declare module "socket.io" {
|
||||
interface Server {
|
||||
_eventBuffer: EventBuffer;
|
||||
_pollingClientsCount: number;
|
||||
}
|
||||
}
|
||||
|
||||
class EventBuffer {
|
||||
private buffer: Map<string, NamespaceEvent> = new Map();
|
||||
|
||||
public push(type: string, subType?: string, count = 1) {
|
||||
const timestamp = new Date();
|
||||
timestamp.setMilliseconds(0);
|
||||
const key = `${timestamp.getTime()};${type};${subType}`;
|
||||
if (this.buffer.has(key)) {
|
||||
this.buffer.get(key)!.count += count;
|
||||
} else {
|
||||
this.buffer.set(key, {
|
||||
timestamp: timestamp.getTime(),
|
||||
type,
|
||||
subType,
|
||||
count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getValuesAndClear() {
|
||||
const values = [...this.buffer.values()];
|
||||
this.buffer.clear();
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
const registerEngineListeners = (io: Server) => {
|
||||
io._eventBuffer = new EventBuffer();
|
||||
io._pollingClientsCount = 0;
|
||||
|
||||
io.engine.on("connection", (rawSocket: any) => {
|
||||
io._eventBuffer.push("rawConnection");
|
||||
|
||||
if (rawSocket.transport.name === "polling") {
|
||||
io._pollingClientsCount++;
|
||||
|
||||
const decr = () => {
|
||||
io._pollingClientsCount--;
|
||||
};
|
||||
|
||||
rawSocket.once("upgrade", () => {
|
||||
rawSocket.removeListener("close", decr);
|
||||
decr();
|
||||
});
|
||||
|
||||
rawSocket.once("close", decr);
|
||||
}
|
||||
|
||||
rawSocket.on("packetCreate", ({ data }: { data: string | Buffer }) => {
|
||||
if (data) {
|
||||
io._eventBuffer.push("packetsOut", undefined);
|
||||
io._eventBuffer.push("bytesOut", undefined, Buffer.byteLength(data));
|
||||
}
|
||||
});
|
||||
|
||||
rawSocket.on("packet", ({ data }: { data: string | Buffer }) => {
|
||||
if (data) {
|
||||
io._eventBuffer.push("packetsIn", undefined);
|
||||
io._eventBuffer.push("bytesIn", undefined, Buffer.byteLength(data));
|
||||
}
|
||||
});
|
||||
|
||||
rawSocket.on("close", (reason: string) => {
|
||||
io._eventBuffer.push("rawDisconnection", reason);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
||||
const options: InstrumentOptions = Object.assign(
|
||||
{
|
||||
|
|
@ -415,6 +506,7 @@ export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
|||
readonly: false,
|
||||
serverId: undefined,
|
||||
store: new InMemoryStore(),
|
||||
mode: process.env.NODE_ENV || "development",
|
||||
},
|
||||
opts
|
||||
);
|
||||
|
|
@ -428,10 +520,13 @@ export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
|||
initAuthenticationMiddleware(adminNamespace, options);
|
||||
|
||||
const supportedFeatures = options.readonly ? [] : detectSupportedFeatures(io);
|
||||
supportedFeatures.push(Feature.AGGREGATED_EVENTS);
|
||||
const isDevelopmentMode = options.mode === "development";
|
||||
if (isDevelopmentMode) {
|
||||
supportedFeatures.push(Feature.ALL_EVENTS);
|
||||
}
|
||||
debug("supported features: %j", supportedFeatures);
|
||||
|
||||
initStatsEmitter(adminNamespace, options.serverId);
|
||||
|
||||
adminNamespace.on("connection", async (socket) => {
|
||||
registerFeatureHandlers(io, socket, supportedFeatures);
|
||||
|
||||
|
|
@ -439,11 +534,22 @@ export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
|||
supportedFeatures,
|
||||
});
|
||||
|
||||
socket.emit("all_sockets", await fetchAllSockets(io));
|
||||
if (isDevelopmentMode) {
|
||||
socket.emit("all_sockets", await fetchAllSockets(io));
|
||||
}
|
||||
});
|
||||
|
||||
io._nsps.forEach((nsp) => registerListeners(adminNamespace, nsp));
|
||||
io.on("new_namespace", (nsp) => registerListeners(adminNamespace, nsp));
|
||||
registerEngineListeners(io);
|
||||
|
||||
if (isDevelopmentMode) {
|
||||
const registerNamespaceListeners = (nsp: Namespace) => {
|
||||
registerVerboseListeners(adminNamespace, nsp);
|
||||
};
|
||||
io._nsps.forEach(registerNamespaceListeners);
|
||||
io.on("new_namespace", registerNamespaceListeners);
|
||||
}
|
||||
|
||||
initStatsEmitter(adminNamespace, options.serverId);
|
||||
}
|
||||
|
||||
export { InMemoryStore, RedisStore } from "./stores";
|
||||
|
|
|
|||
|
|
@ -7,18 +7,35 @@ export enum Feature {
|
|||
MJOIN = "MJOIN",
|
||||
MLEAVE = "MLEAVE",
|
||||
MDISCONNECT = "MDISCONNECT",
|
||||
|
||||
AGGREGATED_EVENTS = "AGGREGATED_EVENTS",
|
||||
ALL_EVENTS = "ALL_EVENTS",
|
||||
}
|
||||
|
||||
interface Config {
|
||||
supportedFeatures: Feature[];
|
||||
}
|
||||
|
||||
export type NamespaceEvent = {
|
||||
timestamp: number;
|
||||
type: string;
|
||||
subType?: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type NamespaceDetails = {
|
||||
name: string;
|
||||
socketsCount: number;
|
||||
};
|
||||
|
||||
interface ServerStats {
|
||||
serverId: string;
|
||||
hostname: string;
|
||||
pid: number;
|
||||
uptime: number;
|
||||
clientsCount: number;
|
||||
pollingClientsCount: number;
|
||||
namespaces: NamespaceDetails[];
|
||||
}
|
||||
|
||||
export interface SerializedSocket {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Server } from "socket.io";
|
|||
import { Server as ServerV3 } from "socket.io-v3";
|
||||
import { io as ioc } from "socket.io-client";
|
||||
import { AddressInfo } from "net";
|
||||
import { InMemoryStore, instrument, RedisStore } from "..";
|
||||
import { InMemoryStore, instrument, RedisStore } from "../lib";
|
||||
import expect = require("expect.js");
|
||||
import { createClient } from "redis";
|
||||
|
||||
|
|
@ -189,6 +189,8 @@ describe("Socket.IO Admin (server instrumentation)", () => {
|
|||
"MJOIN",
|
||||
"MLEAVE",
|
||||
"MDISCONNECT",
|
||||
"AGGREGATED_EVENTS",
|
||||
"ALL_EVENTS",
|
||||
]);
|
||||
} else {
|
||||
expect(config.supportedFeatures).to.eql([
|
||||
|
|
@ -196,6 +198,8 @@ describe("Socket.IO Admin (server instrumentation)", () => {
|
|||
"JOIN",
|
||||
"LEAVE",
|
||||
"DISCONNECT",
|
||||
"AGGREGATED_EVENTS",
|
||||
"ALL_EVENTS",
|
||||
]);
|
||||
}
|
||||
adminSocket.disconnect();
|
||||
|
|
@ -212,7 +216,26 @@ describe("Socket.IO Admin (server instrumentation)", () => {
|
|||
const adminSocket = ioc(`http://localhost:${port}/admin`);
|
||||
|
||||
adminSocket.on("config", (config: any) => {
|
||||
expect(config.supportedFeatures).to.eql([]);
|
||||
expect(config.supportedFeatures).to.eql([
|
||||
"AGGREGATED_EVENTS",
|
||||
"ALL_EVENTS",
|
||||
]);
|
||||
adminSocket.disconnect();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an empty list of supported features when in production mode", (done) => {
|
||||
instrument(io, {
|
||||
auth: false,
|
||||
readonly: true,
|
||||
mode: "production",
|
||||
});
|
||||
|
||||
const adminSocket = ioc(`http://localhost:${port}/admin`);
|
||||
|
||||
adminSocket.on("config", (config: any) => {
|
||||
expect(config.supportedFeatures).to.eql(["AGGREGATED_EVENTS"]);
|
||||
adminSocket.disconnect();
|
||||
done();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
"name": "ui",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"core-js": "^3.6.5",
|
||||
"date-fns": "^2.28.0",
|
||||
"socket.io-msgpack-parser": "^3.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^4.1.1",
|
||||
"vue-i18n": "^8.22.3",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.4.0",
|
||||
|
|
@ -24,7 +27,6 @@
|
|||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
|
|
@ -34,7 +36,6 @@
|
|||
"sass": "^1.32.0",
|
||||
"sass-loader": "^10.0.0",
|
||||
"socket.io-client": "^4.5.0",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-cli-plugin-i18n": "~2.0.3",
|
||||
"vue-cli-plugin-vuetify": "~2.3.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
|
|
@ -1507,15 +1508,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||
|
|
@ -3784,32 +3776,17 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chartjs-color": "^2.1.0",
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
|
||||
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/chartjs-color": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chartjs-color-string": "^0.6.0",
|
||||
"color-convert": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-color-string": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0"
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/check-types": {
|
||||
|
|
@ -5198,6 +5175,18 @@
|
|||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.28.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
|
||||
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
|
@ -9923,15 +9912,6 @@
|
|||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
|
@ -14991,19 +14971,12 @@
|
|||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||
},
|
||||
"node_modules/vue-chartjs": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
|
||||
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/chart.js": "^2.7.55"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-4.1.1.tgz",
|
||||
"integrity": "sha512-rKIQ3jPrjhwxjKdNJppnYxRuBSrx4QeM3nNHsfIxEqjX6QS4Jq6e6vnZBxh2MDpURDC2uvuI2N0eIt1cWXbBVA==",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">= 2.5"
|
||||
"chart.js": "^3.7.0",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-cli-plugin-i18n": {
|
||||
|
|
@ -17948,15 +17921,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.32.tgz",
|
||||
"integrity": "sha512-d45JiRQwEOlZiKwukjqmqpbqbYzUX2yrXdH9qVn6kXpPDsTYCo6YbfFOlnUaJ8S/DhJwbBJiLsMjKpW5oP8B2A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||
|
|
@ -19825,33 +19789,16 @@
|
|||
"dev": true
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chartjs-color": "^2.1.0",
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
|
||||
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==",
|
||||
"peer": true
|
||||
},
|
||||
"chartjs-color": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chartjs-color-string": "^0.6.0",
|
||||
"color-convert": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"chartjs-color-string": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "^1.0.0"
|
||||
}
|
||||
"chartjs-adapter-date-fns": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||
"requires": {}
|
||||
},
|
||||
"check-types": {
|
||||
"version": "8.0.3",
|
||||
|
|
@ -20971,6 +20918,11 @@
|
|||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.28.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
|
||||
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
|
||||
},
|
||||
"de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
|
@ -24695,12 +24647,6 @@
|
|||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
|
@ -28878,13 +28824,10 @@
|
|||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||
},
|
||||
"vue-chartjs": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
|
||||
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/chart.js": "^2.7.55"
|
||||
}
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-4.1.1.tgz",
|
||||
"integrity": "sha512-rKIQ3jPrjhwxjKdNJppnYxRuBSrx4QeM3nNHsfIxEqjX6QS4Jq6e6vnZBxh2MDpURDC2uvuI2N0eIt1cWXbBVA==",
|
||||
"requires": {}
|
||||
},
|
||||
"vue-cli-plugin-i18n": {
|
||||
"version": "2.0.3",
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@
|
|||
"i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\""
|
||||
},
|
||||
"dependencies": {
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"core-js": "^3.6.5",
|
||||
"date-fns": "^2.28.0",
|
||||
"socket.io-msgpack-parser": "^3.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chartjs": "^4.1.1",
|
||||
"vue-i18n": "^8.22.3",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.4.0",
|
||||
|
|
@ -25,7 +28,6 @@
|
|||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
|
|
@ -35,7 +37,6 @@
|
|||
"sass": "^1.32.0",
|
||||
"sass-loader": "^10.0.0",
|
||||
"socket.io-client": "^4.5.0",
|
||||
"vue-chartjs": "^3.5.1",
|
||||
"vue-cli-plugin-i18n": "~2.0.3",
|
||||
"vue-cli-plugin-vuetify": "~2.3.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ export default {
|
|||
});
|
||||
socket.on("server_stats", (serverStats) => {
|
||||
this.$store.commit("servers/onServerStats", serverStats);
|
||||
this.$store.commit("main/onServerStats", serverStats);
|
||||
});
|
||||
socket.on("all_sockets", (sockets) => {
|
||||
this.$store.commit("main/onAllSockets", sockets);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="text-center">
|
||||
{{ $t("dashboard.bytesHistogram.title") }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<Bar
|
||||
:chart-data="chartData"
|
||||
:chart-options="chartOptions"
|
||||
style="width: 100%"
|
||||
:height="chartHeight"
|
||||
/>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import { mapState } from "vuex";
|
||||
import { Bar } from "vue-chartjs/legacy";
|
||||
import { subMinutes } from "date-fns";
|
||||
|
||||
function mapAggregatedEvent(event) {
|
||||
return {
|
||||
x: event.timestamp,
|
||||
y: event.count,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "BytesHistogram",
|
||||
|
||||
components: {
|
||||
Bar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chartHeight: 120,
|
||||
chartOptions: {
|
||||
parsing: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: "minute",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
beginAtZero: true,
|
||||
suggestedMax: 1000,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState("main", ["aggregatedEvents"]),
|
||||
bytesIn() {
|
||||
return this.aggregatedEvents
|
||||
.filter((event) => event.type === "bytesIn")
|
||||
.map(mapAggregatedEvent);
|
||||
},
|
||||
bytesOut() {
|
||||
return this.aggregatedEvents
|
||||
.filter((event) => event.type === "bytesOut")
|
||||
.map(mapAggregatedEvent);
|
||||
},
|
||||
chartData() {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
label: this.$i18n.t("dashboard.bytesHistogram.bytesIn"),
|
||||
backgroundColor: colors.green.base,
|
||||
data: this.bytesIn,
|
||||
},
|
||||
{
|
||||
label: this.$i18n.t("dashboard.bytesHistogram.bytesOut"),
|
||||
backgroundColor: colors.red.base,
|
||||
data: this.bytesOut,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.updateChartBounds();
|
||||
this.interval = setInterval(this.updateChartBounds, 10000);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateChartBounds() {
|
||||
const now = new Date();
|
||||
this.chartOptions.scales.x.min = subMinutes(now, 10);
|
||||
this.chartOptions.scales.x.max = now;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -10,7 +10,11 @@
|
|||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<Doughnut :chart-data="data" class="chart" />
|
||||
<Doughnut
|
||||
:chart-data="data"
|
||||
class="chart"
|
||||
:chart-options="chartOptions"
|
||||
/>
|
||||
|
||||
<v-simple-table class="grow align-self-center">
|
||||
<template v-slot:default>
|
||||
|
|
@ -23,14 +27,11 @@
|
|||
<td><Transport :transport="transport" /></td>
|
||||
<td>
|
||||
<div>
|
||||
<h2>{{ transportRepartition[transport] || 0 }}</h2>
|
||||
<h2>{{ transportRepartition[transport] }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
{{
|
||||
percentage(
|
||||
transportRepartition[transport] || 0,
|
||||
clients.length
|
||||
)
|
||||
percentage(transportRepartition[transport], clientsCount)
|
||||
}}
|
||||
%
|
||||
</div>
|
||||
|
|
@ -45,11 +46,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Doughnut from "./Doughnut";
|
||||
import { mapState } from "vuex";
|
||||
import { Doughnut } from "vue-chartjs/legacy";
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import Transport from "../Transport";
|
||||
import { percentage } from "../../util";
|
||||
import { sumBy } from "lodash-es";
|
||||
|
||||
export default {
|
||||
name: "ClientsOverview",
|
||||
|
|
@ -62,6 +64,13 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
transports: ["websocket", "polling"],
|
||||
chartOptions: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -69,18 +78,38 @@ export default {
|
|||
...mapState({
|
||||
clients: (state) => state.main.clients,
|
||||
darkTheme: (state) => state.config.darkTheme,
|
||||
servers: (state) => state.servers.servers,
|
||||
}),
|
||||
...mapGetters("config", ["hasAggregatedValues"]),
|
||||
|
||||
clientsCount() {
|
||||
if (this.hasAggregatedValues) {
|
||||
return sumBy(this.servers, "clientsCount");
|
||||
} else {
|
||||
return this.clients.length;
|
||||
}
|
||||
},
|
||||
|
||||
transportRepartition() {
|
||||
if (this.hasAggregatedValues) {
|
||||
const pollingClientsCount = sumBy(this.servers, "pollingClientsCount");
|
||||
return {
|
||||
polling: pollingClientsCount,
|
||||
websocket: this.clientsCount - pollingClientsCount,
|
||||
};
|
||||
}
|
||||
return this.clients
|
||||
.map((client) => {
|
||||
return client.sockets[0];
|
||||
})
|
||||
.filter((socket) => !!socket)
|
||||
.reduce((acc, socket) => {
|
||||
acc[socket.transport] = acc[socket.transport] || 0;
|
||||
acc[socket.transport]++;
|
||||
return acc;
|
||||
}, {});
|
||||
.reduce(
|
||||
(acc, socket) => {
|
||||
acc[socket.transport]++;
|
||||
return acc;
|
||||
},
|
||||
{ websocket: 0, polling: 0 }
|
||||
);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="text-center">
|
||||
{{ $t("dashboard.connectionsHistogram.title") }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<Bar
|
||||
:chart-data="chartData"
|
||||
:chart-options="chartOptions"
|
||||
style="width: 100%"
|
||||
:height="chartHeight"
|
||||
/>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import { mapState } from "vuex";
|
||||
import { Bar } from "vue-chartjs/legacy";
|
||||
import { subMinutes } from "date-fns";
|
||||
|
||||
function mapAggregatedEvent(event) {
|
||||
return {
|
||||
x: event.timestamp,
|
||||
y: event.count,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "ConnectionsHistogram",
|
||||
|
||||
components: {
|
||||
Bar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chartHeight: 120,
|
||||
chartOptions: {
|
||||
parsing: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: "minute",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
beginAtZero: true,
|
||||
suggestedMax: 10,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState("main", ["aggregatedEvents"]),
|
||||
connectionEvents() {
|
||||
return this.aggregatedEvents
|
||||
.filter((event) => event.type === "rawConnection")
|
||||
.map(mapAggregatedEvent);
|
||||
},
|
||||
disconnectionEvents() {
|
||||
return this.aggregatedEvents
|
||||
.filter((event) => event.type === "rawDisconnection")
|
||||
.map(mapAggregatedEvent);
|
||||
},
|
||||
chartData() {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
label: this.$i18n.t("events.type.connection"),
|
||||
backgroundColor: colors.green.base,
|
||||
data: this.connectionEvents,
|
||||
},
|
||||
{
|
||||
label: this.$i18n.t("events.type.disconnection"),
|
||||
backgroundColor: colors.red.base,
|
||||
data: this.disconnectionEvents,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.updateChartBounds();
|
||||
this.interval = setInterval(this.updateChartBounds, 10000);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateChartBounds() {
|
||||
const now = new Date();
|
||||
this.chartOptions.scales.x.min = subMinutes(now, 10);
|
||||
this.chartOptions.scales.x.max = now;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { Doughnut, mixins } from "vue-chartjs";
|
||||
const { reactiveProp } = mixins;
|
||||
|
||||
export default {
|
||||
extends: Doughnut,
|
||||
mixins: [reactiveProp],
|
||||
props: ["options"],
|
||||
mounted() {
|
||||
this.renderChart(this.chartData, {
|
||||
legend: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<td class="key-column">
|
||||
<code>{{ namespace.name }}</code>
|
||||
</td>
|
||||
<td>{{ namespace.sockets.length }}</td>
|
||||
<td>{{ namespace.socketsCount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { sortBy } from "lodash-es";
|
||||
|
||||
export default {
|
||||
|
|
@ -38,8 +38,23 @@ export default {
|
|||
|
||||
computed: {
|
||||
...mapState({
|
||||
namespaces: (state) => sortBy(state.main.namespaces, "name"),
|
||||
plainNamespaces: (state) =>
|
||||
sortBy(state.main.namespaces, "name").map(({ name, sockets }) => {
|
||||
return {
|
||||
name,
|
||||
socketsCount: sockets.length,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
...mapGetters("config", ["hasAggregatedValues"]),
|
||||
...mapGetters("servers", {
|
||||
liteNamespaces: "namespaces",
|
||||
}),
|
||||
namespaces() {
|
||||
return this.hasAggregatedValues
|
||||
? this.liteNamespaces
|
||||
: this.plainNamespaces;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@
|
|||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<Doughnut :chart-data="data" class="chart" />
|
||||
<Doughnut
|
||||
:chart-data="data"
|
||||
:chart-options="chartOptions"
|
||||
class="chart"
|
||||
/>
|
||||
|
||||
<v-simple-table class="grow align-self-center">
|
||||
<template v-slot:default>
|
||||
|
|
@ -51,7 +55,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Doughnut from "./Doughnut";
|
||||
import { Doughnut } from "vue-chartjs/legacy";
|
||||
import { mapState } from "vuex";
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import { percentage } from "../../util";
|
||||
|
|
@ -65,6 +69,18 @@ export default {
|
|||
Doughnut,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
chartOptions: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
healthyServers: (state) =>
|
||||
|
|
|
|||
|
|
@ -38,46 +38,64 @@
|
|||
import LangSelector from "./LangSelector";
|
||||
import ThemeSelector from "./ThemeSelector";
|
||||
import ReadonlyToggle from "./ReadonlyToggle";
|
||||
import { mapGetters } from "vuex";
|
||||
export default {
|
||||
name: "NavigationDrawer",
|
||||
|
||||
components: { ReadonlyToggle, ThemeSelector, LangSelector },
|
||||
|
||||
computed: {
|
||||
...mapGetters("config", ["developmentMode"]),
|
||||
items() {
|
||||
return [
|
||||
{
|
||||
title: this.$t("dashboard.title"),
|
||||
icon: "mdi-home-outline",
|
||||
to: { name: "dashboard" },
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
title: this.$t("sockets.title"),
|
||||
icon: "mdi-ray-start-arrow",
|
||||
to: { name: "sockets" },
|
||||
},
|
||||
{
|
||||
title: this.$t("rooms.title"),
|
||||
icon: "mdi-tag-outline",
|
||||
to: { name: "rooms" },
|
||||
},
|
||||
{
|
||||
title: this.$t("clients.title"),
|
||||
icon: "mdi-account-circle-outline",
|
||||
to: { name: "clients" },
|
||||
},
|
||||
{
|
||||
title: this.$t("events.title"),
|
||||
icon: "mdi-calendar-text-outline",
|
||||
to: { name: "events" },
|
||||
},
|
||||
{
|
||||
title: this.$t("servers.title"),
|
||||
icon: "mdi-server",
|
||||
to: { name: "servers" },
|
||||
},
|
||||
];
|
||||
if (this.developmentMode) {
|
||||
return [
|
||||
{
|
||||
title: this.$t("dashboard.title"),
|
||||
icon: "mdi-home-outline",
|
||||
to: { name: "dashboard" },
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
title: this.$t("sockets.title"),
|
||||
icon: "mdi-ray-start-arrow",
|
||||
to: { name: "sockets" },
|
||||
},
|
||||
{
|
||||
title: this.$t("rooms.title"),
|
||||
icon: "mdi-tag-outline",
|
||||
to: { name: "rooms" },
|
||||
},
|
||||
{
|
||||
title: this.$t("clients.title"),
|
||||
icon: "mdi-account-circle-outline",
|
||||
to: { name: "clients" },
|
||||
},
|
||||
{
|
||||
title: this.$t("events.title"),
|
||||
icon: "mdi-calendar-text-outline",
|
||||
to: { name: "events" },
|
||||
},
|
||||
{
|
||||
title: this.$t("servers.title"),
|
||||
icon: "mdi-server",
|
||||
to: { name: "servers" },
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
title: this.$t("dashboard.title"),
|
||||
icon: "mdi-home-outline",
|
||||
to: { name: "dashboard" },
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
title: this.$t("servers.title"),
|
||||
icon: "mdi-server",
|
||||
to: { name: "servers" },
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,7 +32,15 @@
|
|||
"msgpack-parser": "MessagePack parser"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard"
|
||||
"title": "Dashboard",
|
||||
"connectionsHistogram": {
|
||||
"title": "Connection and disconnection events"
|
||||
},
|
||||
"bytesHistogram": {
|
||||
"title": "Bytes received and sent",
|
||||
"bytesIn": "Bytes received",
|
||||
"bytesOut": "Bytes sent"
|
||||
}
|
||||
},
|
||||
"sockets": {
|
||||
"title": "Sockets",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,15 @@
|
|||
"path": "Chemin HTTP"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Accueil"
|
||||
"title": "Accueil",
|
||||
"connectionsHistogram": {
|
||||
"title": "Évènements de connexion et de déconnexion"
|
||||
},
|
||||
"bytesHistogram": {
|
||||
"title": "Octets reçus et envoyés",
|
||||
"bytesIn": "Octets reçus",
|
||||
"bytesOut": "Octets envoyés"
|
||||
}
|
||||
},
|
||||
"sockets": {
|
||||
"title": "Connexions",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import router from "./router";
|
|||
import i18n from "./i18n";
|
||||
import store from "./store";
|
||||
import vuetify from "./plugins/vuetify";
|
||||
import "./plugins/chartjs";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
Chart as ChartJS,
|
||||
DoughnutController,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement,
|
||||
BarElement,
|
||||
TimeScale,
|
||||
LinearScale,
|
||||
} from "chart.js";
|
||||
|
||||
ChartJS.register(
|
||||
DoughnutController,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement,
|
||||
BarElement,
|
||||
TimeScale,
|
||||
LinearScale
|
||||
);
|
||||
|
||||
import "chartjs-adapter-date-fns";
|
||||
|
|
@ -9,6 +9,17 @@ export default {
|
|||
supportedFeatures: [],
|
||||
showNavigationDrawer: false,
|
||||
},
|
||||
getters: {
|
||||
developmentMode(state) {
|
||||
return (
|
||||
state.supportedFeatures.includes("ALL_EVENTS") ||
|
||||
!state.supportedFeatures.includes("AGGREGATED_EVENTS")
|
||||
);
|
||||
},
|
||||
hasAggregatedValues: (state) => {
|
||||
return state.supportedFeatures.includes("AGGREGATED_EVENTS");
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
init(state) {
|
||||
if (isLocalStorageAvailable) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { find, merge } from "lodash-es";
|
||||
import { pushUniq, remove } from "../../util";
|
||||
import { find, merge, remove as silentlyRemove } from "lodash-es";
|
||||
import { pushUniq, remove } from "@/util";
|
||||
|
||||
const TEN_MINUTES = 10 * 60 * 1000;
|
||||
|
||||
const getOrCreateNamespace = (namespaces, name) => {
|
||||
let namespace = find(namespaces, { name });
|
||||
|
|
@ -76,12 +78,19 @@ const pushEvents = (array, event) => {
|
|||
}
|
||||
};
|
||||
|
||||
// group events by each 10 seconds
|
||||
// see: https://www.chartjs.org/docs/latest/general/performance.html#decimation
|
||||
function roundedTimestamp(timestamp) {
|
||||
return timestamp - (timestamp % 10_000);
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
namespaces: [],
|
||||
clients: [],
|
||||
selectedNamespace: null,
|
||||
aggregatedEvents: [],
|
||||
},
|
||||
getters: {
|
||||
findSocketById: (state) => (nsp, id) => {
|
||||
|
|
@ -199,5 +208,31 @@ export default {
|
|||
args: room,
|
||||
});
|
||||
},
|
||||
onServerStats(state, serverStats) {
|
||||
if (!serverStats.aggregatedEvents) {
|
||||
return;
|
||||
}
|
||||
for (const aggregatedEvent of serverStats.aggregatedEvents) {
|
||||
const timestamp = roundedTimestamp(aggregatedEvent.timestamp);
|
||||
const elem = find(state.aggregatedEvents, {
|
||||
timestamp,
|
||||
type: aggregatedEvent.type,
|
||||
subType: aggregatedEvent.subType,
|
||||
});
|
||||
if (elem) {
|
||||
elem.count += aggregatedEvent.count;
|
||||
} else {
|
||||
state.aggregatedEvents.push({
|
||||
timestamp,
|
||||
type: aggregatedEvent.type,
|
||||
subType: aggregatedEvent.subType,
|
||||
count: aggregatedEvent.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
silentlyRemove(state.aggregatedEvents, (elem) => {
|
||||
return elem.timestamp < Date.now() - TEN_MINUTES;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,24 @@ export default {
|
|||
state: {
|
||||
servers: [],
|
||||
},
|
||||
getters: {
|
||||
namespaces(state) {
|
||||
const namespaces = {};
|
||||
for (const server of state.servers) {
|
||||
if (server.namespaces) {
|
||||
for (const { name, socketsCount } of server.namespaces) {
|
||||
namespaces[name] = (namespaces[name] || 0) + socketsCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.keys(namespaces).map((name) => {
|
||||
return {
|
||||
name,
|
||||
socketsCount: namespaces[name],
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
onServerStats(state, stats) {
|
||||
stats.lastPing = Date.now();
|
||||
|
|
|
|||
|
|
@ -4,17 +4,25 @@
|
|||
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col sm="12" md="6" lg="4">
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<ClientsOverview />
|
||||
</v-col>
|
||||
|
||||
<v-col sm="12" md="6" lg="4">
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<ServersOverview />
|
||||
</v-col>
|
||||
|
||||
<v-col sm="12" md="6" lg="4">
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<NamespacesOverview />
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="hasAggregatedValues" cols="12" md="6">
|
||||
<ConnectionsHistogram />
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="hasAggregatedValues" cols="12" md="6">
|
||||
<BytesHistogram />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
|
|
@ -24,11 +32,20 @@
|
|||
import ClientsOverview from "../components/Dashboard/ClientsOverview";
|
||||
import ServersOverview from "../components/Dashboard/ServersOverview";
|
||||
import NamespacesOverview from "../components/Dashboard/NamespacesOverview";
|
||||
import ConnectionsHistogram from "../components/Dashboard/ConnectionsHistogram";
|
||||
import BytesHistogram from "../components/Dashboard/BytesHistogram";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "Dashboard",
|
||||
|
||||
components: { NamespacesOverview, ServersOverview, ClientsOverview },
|
||||
components: {
|
||||
NamespacesOverview,
|
||||
ServersOverview,
|
||||
ClientsOverview,
|
||||
ConnectionsHistogram,
|
||||
BytesHistogram,
|
||||
},
|
||||
|
||||
computed: {
|
||||
breadcrumbItems() {
|
||||
|
|
@ -39,6 +56,7 @@ export default {
|
|||
},
|
||||
];
|
||||
},
|
||||
...mapGetters("config", ["hasAggregatedValues"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue