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 {
|
import {
|
||||||
ClientEvents,
|
ClientEvents,
|
||||||
Feature,
|
Feature,
|
||||||
|
NamespaceDetails,
|
||||||
|
NamespaceEvent,
|
||||||
SerializedSocket,
|
SerializedSocket,
|
||||||
ServerEvents,
|
ServerEvents,
|
||||||
} from "./typed-events";
|
} from "./typed-events";
|
||||||
|
|
@ -46,6 +48,10 @@ interface InstrumentOptions {
|
||||||
* The store
|
* The store
|
||||||
*/
|
*/
|
||||||
store: Store;
|
store: Store;
|
||||||
|
/**
|
||||||
|
* Whether to send all events or only aggregated events to the UI, for performance purposes.
|
||||||
|
*/
|
||||||
|
mode: "development" | "production";
|
||||||
}
|
}
|
||||||
|
|
||||||
const initAuthenticationMiddleware = (
|
const initAuthenticationMiddleware = (
|
||||||
|
|
@ -120,16 +126,26 @@ const initStatsEmitter = (
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const io = adminNamespace.server;
|
||||||
|
|
||||||
const emitStats = () => {
|
const emitStats = () => {
|
||||||
debug("emit stats");
|
debug("emit stats");
|
||||||
// @ts-ignore private reference
|
const namespaces: NamespaceDetails[] = [];
|
||||||
const clientsCount = adminNamespace.server.engine.clientsCount;
|
io._nsps.forEach((namespace) => {
|
||||||
|
namespaces.push({
|
||||||
|
name: namespace.name,
|
||||||
|
socketsCount: namespace.sockets.size,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
adminNamespace.emit(
|
adminNamespace.emit(
|
||||||
"server_stats",
|
"server_stats",
|
||||||
Object.assign({}, baseStats, {
|
Object.assign({}, baseStats, {
|
||||||
uptime: process.uptime(),
|
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>,
|
adminNamespace: Namespace<{}, ServerEvents>,
|
||||||
nsp: Namespace
|
nsp: Namespace
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -407,6 +423,81 @@ const serializeData = (data: any) => {
|
||||||
return obj;
|
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>) {
|
export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
||||||
const options: InstrumentOptions = Object.assign(
|
const options: InstrumentOptions = Object.assign(
|
||||||
{
|
{
|
||||||
|
|
@ -415,6 +506,7 @@ export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
||||||
readonly: false,
|
readonly: false,
|
||||||
serverId: undefined,
|
serverId: undefined,
|
||||||
store: new InMemoryStore(),
|
store: new InMemoryStore(),
|
||||||
|
mode: process.env.NODE_ENV || "development",
|
||||||
},
|
},
|
||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
|
|
@ -428,10 +520,13 @@ export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
||||||
initAuthenticationMiddleware(adminNamespace, options);
|
initAuthenticationMiddleware(adminNamespace, options);
|
||||||
|
|
||||||
const supportedFeatures = options.readonly ? [] : detectSupportedFeatures(io);
|
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);
|
debug("supported features: %j", supportedFeatures);
|
||||||
|
|
||||||
initStatsEmitter(adminNamespace, options.serverId);
|
|
||||||
|
|
||||||
adminNamespace.on("connection", async (socket) => {
|
adminNamespace.on("connection", async (socket) => {
|
||||||
registerFeatureHandlers(io, socket, supportedFeatures);
|
registerFeatureHandlers(io, socket, supportedFeatures);
|
||||||
|
|
||||||
|
|
@ -439,11 +534,22 @@ export function instrument(io: Server, opts: Partial<InstrumentOptions>) {
|
||||||
supportedFeatures,
|
supportedFeatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.emit("all_sockets", await fetchAllSockets(io));
|
if (isDevelopmentMode) {
|
||||||
|
socket.emit("all_sockets", await fetchAllSockets(io));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
io._nsps.forEach((nsp) => registerListeners(adminNamespace, nsp));
|
registerEngineListeners(io);
|
||||||
io.on("new_namespace", (nsp) => registerListeners(adminNamespace, nsp));
|
|
||||||
|
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";
|
export { InMemoryStore, RedisStore } from "./stores";
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,35 @@ export enum Feature {
|
||||||
MJOIN = "MJOIN",
|
MJOIN = "MJOIN",
|
||||||
MLEAVE = "MLEAVE",
|
MLEAVE = "MLEAVE",
|
||||||
MDISCONNECT = "MDISCONNECT",
|
MDISCONNECT = "MDISCONNECT",
|
||||||
|
|
||||||
|
AGGREGATED_EVENTS = "AGGREGATED_EVENTS",
|
||||||
|
ALL_EVENTS = "ALL_EVENTS",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
supportedFeatures: Feature[];
|
supportedFeatures: Feature[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NamespaceEvent = {
|
||||||
|
timestamp: number;
|
||||||
|
type: string;
|
||||||
|
subType?: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NamespaceDetails = {
|
||||||
|
name: string;
|
||||||
|
socketsCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
interface ServerStats {
|
interface ServerStats {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
pid: number;
|
pid: number;
|
||||||
uptime: number;
|
uptime: number;
|
||||||
clientsCount: number;
|
clientsCount: number;
|
||||||
|
pollingClientsCount: number;
|
||||||
|
namespaces: NamespaceDetails[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerializedSocket {
|
export interface SerializedSocket {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Server } from "socket.io";
|
||||||
import { Server as ServerV3 } from "socket.io-v3";
|
import { Server as ServerV3 } from "socket.io-v3";
|
||||||
import { io as ioc } from "socket.io-client";
|
import { io as ioc } from "socket.io-client";
|
||||||
import { AddressInfo } from "net";
|
import { AddressInfo } from "net";
|
||||||
import { InMemoryStore, instrument, RedisStore } from "..";
|
import { InMemoryStore, instrument, RedisStore } from "../lib";
|
||||||
import expect = require("expect.js");
|
import expect = require("expect.js");
|
||||||
import { createClient } from "redis";
|
import { createClient } from "redis";
|
||||||
|
|
||||||
|
|
@ -189,6 +189,8 @@ describe("Socket.IO Admin (server instrumentation)", () => {
|
||||||
"MJOIN",
|
"MJOIN",
|
||||||
"MLEAVE",
|
"MLEAVE",
|
||||||
"MDISCONNECT",
|
"MDISCONNECT",
|
||||||
|
"AGGREGATED_EVENTS",
|
||||||
|
"ALL_EVENTS",
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
expect(config.supportedFeatures).to.eql([
|
expect(config.supportedFeatures).to.eql([
|
||||||
|
|
@ -196,6 +198,8 @@ describe("Socket.IO Admin (server instrumentation)", () => {
|
||||||
"JOIN",
|
"JOIN",
|
||||||
"LEAVE",
|
"LEAVE",
|
||||||
"DISCONNECT",
|
"DISCONNECT",
|
||||||
|
"AGGREGATED_EVENTS",
|
||||||
|
"ALL_EVENTS",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
adminSocket.disconnect();
|
adminSocket.disconnect();
|
||||||
|
|
@ -212,7 +216,26 @@ describe("Socket.IO Admin (server instrumentation)", () => {
|
||||||
const adminSocket = ioc(`http://localhost:${port}/admin`);
|
const adminSocket = ioc(`http://localhost:${port}/admin`);
|
||||||
|
|
||||||
adminSocket.on("config", (config: any) => {
|
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();
|
adminSocket.disconnect();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chartjs-adapter-date-fns": "^2.0.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
"socket.io-msgpack-parser": "^3.0.1",
|
"socket.io-msgpack-parser": "^3.0.1",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
|
"vue-chartjs": "^4.1.1",
|
||||||
"vue-i18n": "^8.22.3",
|
"vue-i18n": "^8.22.3",
|
||||||
"vue-router": "^3.2.0",
|
"vue-router": "^3.2.0",
|
||||||
"vuetify": "^2.4.0",
|
"vuetify": "^2.4.0",
|
||||||
|
|
@ -24,7 +27,6 @@
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"chart.js": "^2.9.4",
|
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
|
@ -34,7 +36,6 @@
|
||||||
"sass": "^1.32.0",
|
"sass": "^1.32.0",
|
||||||
"sass-loader": "^10.0.0",
|
"sass-loader": "^10.0.0",
|
||||||
"socket.io-client": "^4.5.0",
|
"socket.io-client": "^4.5.0",
|
||||||
"vue-chartjs": "^3.5.1",
|
|
||||||
"vue-cli-plugin-i18n": "~2.0.3",
|
"vue-cli-plugin-i18n": "~2.0.3",
|
||||||
"vue-cli-plugin-vuetify": "~2.3.1",
|
"vue-cli-plugin-vuetify": "~2.3.1",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
|
@ -1507,15 +1508,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.34",
|
"version": "3.4.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||||
|
|
@ -3784,32 +3776,17 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "2.9.4",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
|
||||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==",
|
||||||
"dev": true,
|
"peer": true
|
||||||
"dependencies": {
|
|
||||||
"chartjs-color": "^2.1.0",
|
|
||||||
"moment": "^2.10.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/chartjs-color": {
|
"node_modules/chartjs-adapter-date-fns": {
|
||||||
"version": "2.4.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||||
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||||
"dev": true,
|
"peerDependencies": {
|
||||||
"dependencies": {
|
"chart.js": "^3.0.0"
|
||||||
"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/check-types": {
|
"node_modules/check-types": {
|
||||||
|
|
@ -5198,6 +5175,18 @@
|
||||||
"node": ">=0.10"
|
"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": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
|
@ -9923,15 +9912,6 @@
|
||||||
"mkdirp": "bin/cmd.js"
|
"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": {
|
"node_modules/move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
|
|
@ -14991,19 +14971,12 @@
|
||||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||||
},
|
},
|
||||||
"node_modules/vue-chartjs": {
|
"node_modules/vue-chartjs": {
|
||||||
"version": "3.5.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-4.1.1.tgz",
|
||||||
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
|
"integrity": "sha512-rKIQ3jPrjhwxjKdNJppnYxRuBSrx4QeM3nNHsfIxEqjX6QS4Jq6e6vnZBxh2MDpURDC2uvuI2N0eIt1cWXbBVA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/chart.js": "^2.7.55"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0",
|
|
||||||
"npm": ">= 3.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"chart.js": ">= 2.5"
|
"chart.js": "^3.7.0",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-cli-plugin-i18n": {
|
"node_modules/vue-cli-plugin-i18n": {
|
||||||
|
|
@ -17948,15 +17921,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"@types/connect": {
|
||||||
"version": "3.4.34",
|
"version": "3.4.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||||
|
|
@ -19825,33 +19789,16 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"chart.js": {
|
"chart.js": {
|
||||||
"version": "2.9.4",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
|
||||||
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
"integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg==",
|
||||||
"dev": true,
|
"peer": true
|
||||||
"requires": {
|
|
||||||
"chartjs-color": "^2.1.0",
|
|
||||||
"moment": "^2.10.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"chartjs-color": {
|
"chartjs-adapter-date-fns": {
|
||||||
"version": "2.4.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz",
|
||||||
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
"integrity": "sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==",
|
||||||
"dev": true,
|
"requires": {}
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"check-types": {
|
"check-types": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.3",
|
||||||
|
|
@ -20971,6 +20918,11 @@
|
||||||
"assert-plus": "^1.0.0"
|
"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": {
|
"de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
|
@ -24695,12 +24647,6 @@
|
||||||
"minimist": "^1.2.5"
|
"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": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
|
|
@ -28878,13 +28824,10 @@
|
||||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||||
},
|
},
|
||||||
"vue-chartjs": {
|
"vue-chartjs": {
|
||||||
"version": "3.5.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-3.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-4.1.1.tgz",
|
||||||
"integrity": "sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==",
|
"integrity": "sha512-rKIQ3jPrjhwxjKdNJppnYxRuBSrx4QeM3nNHsfIxEqjX6QS4Jq6e6vnZBxh2MDpURDC2uvuI2N0eIt1cWXbBVA==",
|
||||||
"dev": true,
|
"requires": {}
|
||||||
"requires": {
|
|
||||||
"@types/chart.js": "^2.7.55"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"vue-cli-plugin-i18n": {
|
"vue-cli-plugin-i18n": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@
|
||||||
"i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\""
|
"i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chartjs-adapter-date-fns": "^2.0.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
"socket.io-msgpack-parser": "^3.0.1",
|
"socket.io-msgpack-parser": "^3.0.1",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
|
"vue-chartjs": "^4.1.1",
|
||||||
"vue-i18n": "^8.22.3",
|
"vue-i18n": "^8.22.3",
|
||||||
"vue-router": "^3.2.0",
|
"vue-router": "^3.2.0",
|
||||||
"vuetify": "^2.4.0",
|
"vuetify": "^2.4.0",
|
||||||
|
|
@ -25,7 +28,6 @@
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"chart.js": "^2.9.4",
|
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
|
@ -35,7 +37,6 @@
|
||||||
"sass": "^1.32.0",
|
"sass": "^1.32.0",
|
||||||
"sass-loader": "^10.0.0",
|
"sass-loader": "^10.0.0",
|
||||||
"socket.io-client": "^4.5.0",
|
"socket.io-client": "^4.5.0",
|
||||||
"vue-chartjs": "^3.5.1",
|
|
||||||
"vue-cli-plugin-i18n": "~2.0.3",
|
"vue-cli-plugin-i18n": "~2.0.3",
|
||||||
"vue-cli-plugin-vuetify": "~2.3.1",
|
"vue-cli-plugin-vuetify": "~2.3.1",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ export default {
|
||||||
});
|
});
|
||||||
socket.on("server_stats", (serverStats) => {
|
socket.on("server_stats", (serverStats) => {
|
||||||
this.$store.commit("servers/onServerStats", serverStats);
|
this.$store.commit("servers/onServerStats", serverStats);
|
||||||
|
this.$store.commit("main/onServerStats", serverStats);
|
||||||
});
|
});
|
||||||
socket.on("all_sockets", (sockets) => {
|
socket.on("all_sockets", (sockets) => {
|
||||||
this.$store.commit("main/onAllSockets", 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-card-text>
|
||||||
<v-row>
|
<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">
|
<v-simple-table class="grow align-self-center">
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
|
|
@ -23,14 +27,11 @@
|
||||||
<td><Transport :transport="transport" /></td>
|
<td><Transport :transport="transport" /></td>
|
||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ transportRepartition[transport] || 0 }}</h2>
|
<h2>{{ transportRepartition[transport] }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{
|
{{
|
||||||
percentage(
|
percentage(transportRepartition[transport], clientsCount)
|
||||||
transportRepartition[transport] || 0,
|
|
||||||
clients.length
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
%
|
%
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,11 +46,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Doughnut from "./Doughnut";
|
import { Doughnut } from "vue-chartjs/legacy";
|
||||||
import { mapState } from "vuex";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import colors from "vuetify/lib/util/colors";
|
import colors from "vuetify/lib/util/colors";
|
||||||
import Transport from "../Transport";
|
import Transport from "../Transport";
|
||||||
import { percentage } from "../../util";
|
import { percentage } from "../../util";
|
||||||
|
import { sumBy } from "lodash-es";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ClientsOverview",
|
name: "ClientsOverview",
|
||||||
|
|
@ -62,6 +64,13 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
transports: ["websocket", "polling"],
|
transports: ["websocket", "polling"],
|
||||||
|
chartOptions: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -69,18 +78,38 @@ export default {
|
||||||
...mapState({
|
...mapState({
|
||||||
clients: (state) => state.main.clients,
|
clients: (state) => state.main.clients,
|
||||||
darkTheme: (state) => state.config.darkTheme,
|
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() {
|
transportRepartition() {
|
||||||
|
if (this.hasAggregatedValues) {
|
||||||
|
const pollingClientsCount = sumBy(this.servers, "pollingClientsCount");
|
||||||
|
return {
|
||||||
|
polling: pollingClientsCount,
|
||||||
|
websocket: this.clientsCount - pollingClientsCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
return this.clients
|
return this.clients
|
||||||
.map((client) => {
|
.map((client) => {
|
||||||
return client.sockets[0];
|
return client.sockets[0];
|
||||||
})
|
})
|
||||||
.filter((socket) => !!socket)
|
.filter((socket) => !!socket)
|
||||||
.reduce((acc, socket) => {
|
.reduce(
|
||||||
acc[socket.transport] = acc[socket.transport] || 0;
|
(acc, socket) => {
|
||||||
acc[socket.transport]++;
|
acc[socket.transport]++;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
},
|
||||||
|
{ websocket: 0, polling: 0 }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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">
|
<td class="key-column">
|
||||||
<code>{{ namespace.name }}</code>
|
<code>{{ namespace.name }}</code>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ namespace.sockets.length }}</td>
|
<td>{{ namespace.socketsCount }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "vuex";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -38,8 +38,23 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<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">
|
<v-simple-table class="grow align-self-center">
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
|
|
@ -51,7 +55,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Doughnut from "./Doughnut";
|
import { Doughnut } from "vue-chartjs/legacy";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import colors from "vuetify/lib/util/colors";
|
import colors from "vuetify/lib/util/colors";
|
||||||
import { percentage } from "../../util";
|
import { percentage } from "../../util";
|
||||||
|
|
@ -65,6 +69,18 @@ export default {
|
||||||
Doughnut,
|
Doughnut,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chartOptions: {
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
healthyServers: (state) =>
|
healthyServers: (state) =>
|
||||||
|
|
|
||||||
|
|
@ -38,46 +38,64 @@
|
||||||
import LangSelector from "./LangSelector";
|
import LangSelector from "./LangSelector";
|
||||||
import ThemeSelector from "./ThemeSelector";
|
import ThemeSelector from "./ThemeSelector";
|
||||||
import ReadonlyToggle from "./ReadonlyToggle";
|
import ReadonlyToggle from "./ReadonlyToggle";
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
export default {
|
export default {
|
||||||
name: "NavigationDrawer",
|
name: "NavigationDrawer",
|
||||||
|
|
||||||
components: { ReadonlyToggle, ThemeSelector, LangSelector },
|
components: { ReadonlyToggle, ThemeSelector, LangSelector },
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters("config", ["developmentMode"]),
|
||||||
items() {
|
items() {
|
||||||
return [
|
if (this.developmentMode) {
|
||||||
{
|
return [
|
||||||
title: this.$t("dashboard.title"),
|
{
|
||||||
icon: "mdi-home-outline",
|
title: this.$t("dashboard.title"),
|
||||||
to: { name: "dashboard" },
|
icon: "mdi-home-outline",
|
||||||
exact: true,
|
to: { name: "dashboard" },
|
||||||
},
|
exact: true,
|
||||||
{
|
},
|
||||||
title: this.$t("sockets.title"),
|
{
|
||||||
icon: "mdi-ray-start-arrow",
|
title: this.$t("sockets.title"),
|
||||||
to: { name: "sockets" },
|
icon: "mdi-ray-start-arrow",
|
||||||
},
|
to: { name: "sockets" },
|
||||||
{
|
},
|
||||||
title: this.$t("rooms.title"),
|
{
|
||||||
icon: "mdi-tag-outline",
|
title: this.$t("rooms.title"),
|
||||||
to: { name: "rooms" },
|
icon: "mdi-tag-outline",
|
||||||
},
|
to: { name: "rooms" },
|
||||||
{
|
},
|
||||||
title: this.$t("clients.title"),
|
{
|
||||||
icon: "mdi-account-circle-outline",
|
title: this.$t("clients.title"),
|
||||||
to: { name: "clients" },
|
icon: "mdi-account-circle-outline",
|
||||||
},
|
to: { name: "clients" },
|
||||||
{
|
},
|
||||||
title: this.$t("events.title"),
|
{
|
||||||
icon: "mdi-calendar-text-outline",
|
title: this.$t("events.title"),
|
||||||
to: { name: "events" },
|
icon: "mdi-calendar-text-outline",
|
||||||
},
|
to: { name: "events" },
|
||||||
{
|
},
|
||||||
title: this.$t("servers.title"),
|
{
|
||||||
icon: "mdi-server",
|
title: this.$t("servers.title"),
|
||||||
to: { name: "servers" },
|
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"
|
"msgpack-parser": "MessagePack parser"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard"
|
"title": "Dashboard",
|
||||||
|
"connectionsHistogram": {
|
||||||
|
"title": "Connection and disconnection events"
|
||||||
|
},
|
||||||
|
"bytesHistogram": {
|
||||||
|
"title": "Bytes received and sent",
|
||||||
|
"bytesIn": "Bytes received",
|
||||||
|
"bytesOut": "Bytes sent"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sockets": {
|
"sockets": {
|
||||||
"title": "Sockets",
|
"title": "Sockets",
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,15 @@
|
||||||
"path": "Chemin HTTP"
|
"path": "Chemin HTTP"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"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": {
|
"sockets": {
|
||||||
"title": "Connexions",
|
"title": "Connexions",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import router from "./router";
|
||||||
import i18n from "./i18n";
|
import i18n from "./i18n";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
import vuetify from "./plugins/vuetify";
|
import vuetify from "./plugins/vuetify";
|
||||||
|
import "./plugins/chartjs";
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
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: [],
|
supportedFeatures: [],
|
||||||
showNavigationDrawer: false,
|
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: {
|
mutations: {
|
||||||
init(state) {
|
init(state) {
|
||||||
if (isLocalStorageAvailable) {
|
if (isLocalStorageAvailable) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { find, merge } from "lodash-es";
|
import { find, merge, remove as silentlyRemove } from "lodash-es";
|
||||||
import { pushUniq, remove } from "../../util";
|
import { pushUniq, remove } from "@/util";
|
||||||
|
|
||||||
|
const TEN_MINUTES = 10 * 60 * 1000;
|
||||||
|
|
||||||
const getOrCreateNamespace = (namespaces, name) => {
|
const getOrCreateNamespace = (namespaces, name) => {
|
||||||
let namespace = find(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 {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
namespaces: [],
|
namespaces: [],
|
||||||
clients: [],
|
clients: [],
|
||||||
selectedNamespace: null,
|
selectedNamespace: null,
|
||||||
|
aggregatedEvents: [],
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
findSocketById: (state) => (nsp, id) => {
|
findSocketById: (state) => (nsp, id) => {
|
||||||
|
|
@ -199,5 +208,31 @@ export default {
|
||||||
args: room,
|
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: {
|
state: {
|
||||||
servers: [],
|
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: {
|
mutations: {
|
||||||
onServerStats(state, stats) {
|
onServerStats(state, stats) {
|
||||||
stats.lastPing = Date.now();
|
stats.lastPing = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,25 @@
|
||||||
|
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col sm="12" md="6" lg="4">
|
<v-col cols="12" md="6" lg="4">
|
||||||
<ClientsOverview />
|
<ClientsOverview />
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col sm="12" md="6" lg="4">
|
<v-col cols="12" md="6" lg="4">
|
||||||
<ServersOverview />
|
<ServersOverview />
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col sm="12" md="6" lg="4">
|
<v-col cols="12" md="6" lg="4">
|
||||||
<NamespacesOverview />
|
<NamespacesOverview />
|
||||||
</v-col>
|
</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-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24,11 +32,20 @@
|
||||||
import ClientsOverview from "../components/Dashboard/ClientsOverview";
|
import ClientsOverview from "../components/Dashboard/ClientsOverview";
|
||||||
import ServersOverview from "../components/Dashboard/ServersOverview";
|
import ServersOverview from "../components/Dashboard/ServersOverview";
|
||||||
import NamespacesOverview from "../components/Dashboard/NamespacesOverview";
|
import NamespacesOverview from "../components/Dashboard/NamespacesOverview";
|
||||||
|
import ConnectionsHistogram from "../components/Dashboard/ConnectionsHistogram";
|
||||||
|
import BytesHistogram from "../components/Dashboard/BytesHistogram";
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
|
|
||||||
components: { NamespacesOverview, ServersOverview, ClientsOverview },
|
components: {
|
||||||
|
NamespacesOverview,
|
||||||
|
ServersOverview,
|
||||||
|
ClientsOverview,
|
||||||
|
ConnectionsHistogram,
|
||||||
|
BytesHistogram,
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
breadcrumbItems() {
|
breadcrumbItems() {
|
||||||
|
|
@ -39,6 +56,7 @@ export default {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
...mapGetters("config", ["hasAggregatedValues"]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue