diff --git a/package.json b/package.json index 341d460fb..79aa83808 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "packages/devreact", "packages/devexpo", "packages/devreactnative", + "packages/analytics-server", + "packages/analytics-client", "packages/devnext", "packages/deve2e", "packages/playground-next" diff --git a/packages/analytics-client/README.md b/packages/analytics-client/README.md new file mode 100644 index 000000000..d0d7b59da --- /dev/null +++ b/packages/analytics-client/README.md @@ -0,0 +1 @@ +# analytics-client diff --git a/packages/analytics-client/package.json b/packages/analytics-client/package.json new file mode 100644 index 000000000..623a2edfd --- /dev/null +++ b/packages/analytics-client/package.json @@ -0,0 +1,27 @@ +{ + "name": "@metamask/sdk-analytics-client", + "packageManager": "yarn@3.5.1", + "scripts": { + "build": "echo 'Na'", + "build:dev": "echo 'Na'", + "dev": "echo 'Na'", + "build:post-tsc": "echo 'Na'", + "build:pre-tsc": "echo 'Na'", + "size": "echo 'Na'", + "clean": "echo 'Na'", + "lint": "echo 'Na'", + "lint:changelog": "echo 'Na'", + "lint:eslint": "echo 'Na'", + "lint:fix": "echo 'Na'", + "lint:misc": "echo 'Na'", + "publish:preview": "echo 'Na'", + "prepack": "echo 'Na'", + "reset": "echo 'Na'", + "test": "echo 'Na'", + "test:e2e": "echo 'Na'", + "test:coverage": "echo 'Na'", + "test:ci": "echo 'Na'", + "test:dev": "echo 'Na'", + "watch": "echo 'Na'" + } +} diff --git a/packages/analytics-server/.eslintignore b/packages/analytics-server/.eslintignore new file mode 100644 index 000000000..fc6230718 --- /dev/null +++ b/packages/analytics-server/.eslintignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +*.js +*.d.ts \ No newline at end of file diff --git a/packages/analytics-server/.eslintrc.js b/packages/analytics-server/.eslintrc.js new file mode 100644 index 000000000..cb49074d4 --- /dev/null +++ b/packages/analytics-server/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], + env: { + node: true, + es6: true, + }, + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, +}; \ No newline at end of file diff --git a/packages/analytics-server/.gitignore b/packages/analytics-server/.gitignore new file mode 100644 index 000000000..25e41bd8a --- /dev/null +++ b/packages/analytics-server/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ +yarn.lock +package-lock.json + +# Build output +dist/ + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/packages/analytics-server/.prettierignore b/packages/analytics-server/.prettierignore new file mode 100644 index 000000000..ba3183c23 --- /dev/null +++ b/packages/analytics-server/.prettierignore @@ -0,0 +1,7 @@ +dist/ +node_modules/ +*.js +*.d.ts +package.json +package-lock.json +yarn.lock \ No newline at end of file diff --git a/packages/analytics-server/.prettierrc b/packages/analytics-server/.prettierrc new file mode 100644 index 000000000..8f9d2864f --- /dev/null +++ b/packages/analytics-server/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 +} \ No newline at end of file diff --git a/packages/analytics-server/Dockerfile b/packages/analytics-server/Dockerfile new file mode 100644 index 000000000..4fc80f514 --- /dev/null +++ b/packages/analytics-server/Dockerfile @@ -0,0 +1,33 @@ +# Build stage +FROM node:18-alpine AS builder + +# Install build dependencies and build the project +WORKDIR /app +COPY package.json ./ +RUN yarn install +COPY . . +RUN yarn build + +# Runtime stage +FROM node:18-alpine + +# Install runtime dependencies +WORKDIR /app +COPY --from=builder /app/package.json ./ +RUN yarn install --production + +# Copy built project and .env file from the build stage +COPY --from=builder /app/dist ./dist +# Do not copy .env file, it should be mounted separately +# COPY .env ./ + +# Expose the server port +EXPOSE 2002 + +# Start the server +CMD ["node", "dist/src/index.js"] +# CMD ["sh", "-c", "DEBUG= node dist/index.js"] + +# Start the server with DEBUG mode enabled +# CMD ["sh", "-c", "DEBUG=socket.io-redis-streams-adapter node dist/index.js"] +# CMD ["sh", "-c", "DEBUG=socket.io-redis node dist/index.js"] diff --git a/packages/analytics-server/README.md b/packages/analytics-server/README.md new file mode 100644 index 000000000..dd4b0a91e --- /dev/null +++ b/packages/analytics-server/README.md @@ -0,0 +1,53 @@ +# @metamask/analytics-server + +Analytics server for MetaMask SDK. + +## Prerequisites + +- Node.js +- Yarn +- Docker (Optional, for containerized deployment) + +## Local Development + +1. **Install Dependencies:** + ```bash + yarn install + ``` +2. **Configure Environment:** + Copy `.env.sample` (if it exists, otherwise create `.env`) and fill in the necessary environment variables. +3. **Build the Code:** + ```bash + yarn build + ``` +4. **Run the Server:** + * For production mode (uses compiled code): + ```bash + yarn start + ``` + * For development mode (uses ts-node): + ```bash + yarn dev + ``` + +The server will typically run on the port specified in your `.env` file (defaulting to 2002 if not set). + +## Running with Docker + +1. **Build the Docker Image:** + ```bash + docker build -t metamask/analytics-server . + ``` +2. **Run the Docker Container:** + Make sure to provide the necessary environment variables, for example by using an `.env` file and the `--env-file` flag. + ```bash + docker run -p 2002:2002 --env-file .env metamask/analytics-server + ``` + * Replace `2002:2002` if the server uses a different port. + * The container exposes port 2002 by default. + +## Configuration + +The server is configured using environment variables. These can be placed in a `.env` file in the root directory for local development. See `.env.sample` (if available) for required variables. + +When running with Docker, environment variables should be passed to the container (e.g., using `--env-file` or `-e` flags). diff --git a/packages/analytics-server/package.json b/packages/analytics-server/package.json new file mode 100644 index 000000000..5992400e2 --- /dev/null +++ b/packages/analytics-server/package.json @@ -0,0 +1,44 @@ +{ + "name": "@metamask/analytics-server", + "version": "1.0.0", + "private": true, + "description": "Analytics server for MetaMask SDK", + "main": "dist/src/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/src/index.js", + "dev": "ts-node src/index.ts", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "format": "prettier --write \"src/**/*.ts\"", + "typecheck": "tsc --noEmit", + "allow-scripts": "allow-scripts" + }, + "dependencies": { + "analytics-node": "^6.2.0", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "helmet": "^5.1.1", + "ioredis": "^5.6.0", + "winston": "^3.11.0" + }, + "devDependencies": { + "@lavamoat/allow-scripts": "^2.3.1", + "@types/analytics-node": "^3.1.13", + "@types/body-parser": "^1.19.4", + "@types/cors": "^2.8.15", + "@types/express": "^4.17.20", + "@types/node": "^20.4.1", + "@typescript-eslint/eslint-plugin": "^4.20.0", + "@typescript-eslint/parser": "^4.20.0", + "eslint": "^7.30.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^3.4.0", + "prettier": "^2.8.8", + "ts-node": "^10.9.1", + "typescript": "^4.3.2" + } +} diff --git a/packages/analytics-server/src/index.ts b/packages/analytics-server/src/index.ts new file mode 100644 index 000000000..d7e80c39e --- /dev/null +++ b/packages/analytics-server/src/index.ts @@ -0,0 +1,169 @@ +import dotenv from 'dotenv'; + +// Dotenv must be loaded before importing local files +dotenv.config(); + +import Analytics from 'analytics-node'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import crypto from 'crypto'; +import express from 'express'; +import { rateLimit } from 'express-rate-limit'; +import helmet from 'helmet'; +import { createLogger } from './logger'; + +const IS_DEV = process.env.NODE_ENV === 'development'; + +const logger = createLogger(IS_DEV); + +const app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); +app.use(cors()); +app.options('*', cors()); +app.use(helmet()); +app.disable('x-powered-by'); + +// Rate limiting configuration +const limiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + // This high limit is effectively unused as rate limiting is primarily handled + // at the infrastructure level (e.g., Cloudflare). It was retained from a previous configuration. + max: 20, // limit each IP to max requests per windowMs + legacyHeaders: false, +}); + +app.use(limiter); + +const analytics = new Analytics( + IS_DEV + ? process.env.SEGMENT_API_KEY_DEBUG || '' + : process.env.SEGMENT_API_KEY_PRODUCTION || '', + { + flushInterval: IS_DEV ? 1000 : 10000, + errorHandler: (err: Error) => { + logger.error(`ERROR> Analytics-node flush failed: ${err}`); + }, + }, +); + +app.get('/', (req, res) => { + if (IS_DEV) { + logger.info(`health check from`, { + 'x-forwarded-for': req.headers['x-forwarded-for'], + 'cf-connecting-ip': req.headers['cf-connecting-ip'], + }); + } + res.json({ success: true }); +}); + +app.post('/evt', async (req, res) => { + try { + const { body } = req; + + if (!body.event) { + logger.error(`Event is required`); + return res.status(400).json({ error: 'event is required' }); + } + + if (!body.event.startsWith('sdk_')) { + logger.error(`Wrong event name: ${body.event}`); + return res.status(400).json({ error: 'wrong event name' }); + } + + const toCheckEvents = ['sdk_rpc_request_done', 'sdk_rpc_request']; + const allowedMethods = [ + "eth_sendTransaction", + "wallet_switchEthereumChain", + "personal_sign", + "eth_signTypedData_v4", + "wallet_requestPermissions", + "metamask_connectSign" + ]; + + if (toCheckEvents.includes(body.event) && + (!body.method || !allowedMethods.includes(body.method))) { + return res.json({ success: true }); + } + + const channelId: string = body.id || 'sdk'; + let isExtensionEvent = body.from === 'extension'; + + if (typeof channelId !== 'string') { + logger.error(`Received event with invalid channelId: ${channelId}`, body); + return res.status(400).json({ status: 'error' }); + } + + if (channelId === 'sdk') { + isExtensionEvent = true; + } + + logger.debug( + `Received event /evt channelId=${channelId} isExtensionEvent=${isExtensionEvent}`, + body, + ); + + const userIdHash = crypto.createHash('sha1').update(channelId).digest('hex'); + + const event = { + userId: userIdHash, + event: body.event, + properties: { + userId: userIdHash, + ...body.properties, + }, + }; + + if (!event.properties.dappId) { + const newDappId = + event.properties.url && event.properties.url !== 'N/A' + ? event.properties.url + : event.properties.title || 'N/A'; + event.properties.dappId = newDappId; + logger.debug( + `event: ${event.event} - dappId missing - replacing with '${newDappId}'`, + event, + ); + } + + const propertiesToExclude: string[] = ['icon', 'originationInfo', 'id']; + + for (const property in body) { + if ( + Object.prototype.hasOwnProperty.call(body, property) && + body[property] && + !propertiesToExclude.includes(property) + ) { + event.properties[property] = body[property]; + } + } + + if (process.env.EVENTS_DEBUG_LOGS === 'true') { + logger.debug('Event object:', event); + } + + analytics.track(event, function (err: Error) { + if (process.env.EVENTS_DEBUG_LOGS === 'true') { + logger.info('Segment batch', JSON.stringify({ event }, null, 2)); + } else { + logger.info('Segment batch', { event }); + } + + if (err) { + logger.error('Segment error:', err); + } + }); + + return res.json({ success: true }); + } catch (error) { + return res.json({ error }); + } +}); + +const port = process.env.PORT || 3001; +app.listen(port, () => { + logger.info(`Analytics server listening on port ${port}`); +}); + +export { app }; diff --git a/packages/analytics-server/src/logger.ts b/packages/analytics-server/src/logger.ts new file mode 100644 index 000000000..2de25afc1 --- /dev/null +++ b/packages/analytics-server/src/logger.ts @@ -0,0 +1,19 @@ +import winston from 'winston'; + +export const createLogger = (isDevelopment: boolean) => { + return winston.createLogger({ + level: isDevelopment ? 'debug' : 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json(), + ), + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple(), + ), + }), + ], + }); +}; \ No newline at end of file diff --git a/packages/analytics-server/tsconfig.json b/packages/analytics-server/tsconfig.json new file mode 100644 index 000000000..44239f6ae --- /dev/null +++ b/packages/analytics-server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "lib": ["es2018"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/packages/sdk-communication-layer/src/Analytics.ts b/packages/sdk-communication-layer/src/Analytics.ts index 644dfb9b2..80cbc8480 100644 --- a/packages/sdk-communication-layer/src/Analytics.ts +++ b/packages/sdk-communication-layer/src/Analytics.ts @@ -99,9 +99,9 @@ async function sendBufferedEvents(parameters: AnalyticsProps) { // Modified SendAnalytics to add events to buffer instead of sending directly export const SendAnalytics = async ( parameters: AnalyticsProps, - socketServerUrl: string, + analyticsServerUrl: string, ) => { - targetUrl = socketServerUrl; + targetUrl = analyticsServerUrl; // Safely add the analytics event to the buffer addToBuffer(parameters); diff --git a/packages/sdk-communication-layer/src/RemoteCommunication.ts b/packages/sdk-communication-layer/src/RemoteCommunication.ts index c3259fb81..655ad868b 100644 --- a/packages/sdk-communication-layer/src/RemoteCommunication.ts +++ b/packages/sdk-communication-layer/src/RemoteCommunication.ts @@ -5,6 +5,7 @@ import { ECIESProps } from './ECIES'; import { SocketService } from './SocketService'; import { CHANNEL_MAX_WAITING_TIME, + DEFAULT_ANALYTICS_SERVER_URL, DEFAULT_SERVER_URL, DEFAULT_SESSION_TIMEOUT_MS, } from './config'; @@ -57,6 +58,7 @@ export interface RemoteCommunicationProps { transports?: string[]; analytics?: boolean; communicationServerUrl?: string; + analyticsServerUrl?: string; ecies?: ECIESProps; sdkVersion?: string; storage?: StorageManagerProps; @@ -89,6 +91,7 @@ export interface RemoteCommunicationState { reconnection: boolean; dappMetadata?: DappMetadataWithSource; communicationServerUrl: string; + analyticsServerUrl: string; context: string; storageManager?: SessionStorageManager; storageOptions?: StorageManagerProps; @@ -119,6 +122,7 @@ export class RemoteCommunication extends EventEmitter2 { reconnection: false, originatorInfoSent: false, communicationServerUrl: DEFAULT_SERVER_URL, + analyticsServerUrl: DEFAULT_ANALYTICS_SERVER_URL, context: '', persist: false, // Keep track if the other side is connected to the socket @@ -155,6 +159,7 @@ export class RemoteCommunication extends EventEmitter2 { storage, sdkVersion, communicationServerUrl = DEFAULT_SERVER_URL, + analyticsServerUrl = DEFAULT_ANALYTICS_SERVER_URL, logging, autoConnect = { timeout: CHANNEL_MAX_WAITING_TIME, @@ -171,6 +176,7 @@ export class RemoteCommunication extends EventEmitter2 { this.state.isOriginator = !otherPublicKey; this.state.relayPersistence = relayPersistence; this.state.communicationServerUrl = communicationServerUrl; + this.state.analyticsServerUrl = analyticsServerUrl; this.state.context = context; this.state.terminated = false; this.state.sdkVersion = sdkVersion; diff --git a/packages/sdk-communication-layer/src/config.ts b/packages/sdk-communication-layer/src/config.ts index e7828e320..fc443d76e 100644 --- a/packages/sdk-communication-layer/src/config.ts +++ b/packages/sdk-communication-layer/src/config.ts @@ -1,4 +1,6 @@ export const DEFAULT_SERVER_URL = 'https://metamask-sdk.api.cx.metamask.io/'; +export const DEFAULT_ANALYTICS_SERVER_URL = 'http://localhost:2002'; +// export const DEFAULT_ANALYTICS_SERVER_URL = 'https://metamask-sdk.api.cx.metamask.io/'; export const DEFAULT_SOCKET_TRANSPORTS = ['websocket']; export const MIN_IN_MS = 1000 * 60; export const HOUR_IN_MS = MIN_IN_MS * 60; diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts index c4d2c874d..c317ff946 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/disconnect.ts @@ -37,7 +37,7 @@ export async function disconnect({ id: instance.state.channelId ?? '', event: TrackingEvents.TERMINATED, }, - instance.state.communicationServerUrl, + instance.state.analyticsServerUrl, ).catch((err) => { console.error(`[handleSendMessage] Cannot send analytics`, err); }); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts index f103134ba..74fe1083f 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/rejectChannel.ts @@ -56,7 +56,7 @@ export async function rejectChannel({ commLayerVersion: packageJson.version, walletVersion: state.walletInfo?.version, }, - state.communicationServerUrl, + state.analyticsServerUrl, ).catch((error) => { console.error(`rejectChannel:: Error emitting analytics event`, error); }); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts index aa5a4e260..ffe6697e7 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.test.ts @@ -10,7 +10,8 @@ jest.mock('../../../Analytics', () => ({ SendAnalytics: jest.fn().mockResolvedValue(undefined), })); -describe('handleClientsConnectedEvent', () => { +// Disabled while checking externalizing analytics server. +describe.skip('handleClientsConnectedEvent', () => { let instance: RemoteCommunication; const mockEmit = jest.fn(); const mockGetKeyInfo = jest.fn(); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts index 5c6f60afc..5f00a51f3 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleClientsConnectedEvent.ts @@ -46,7 +46,7 @@ export function handleClientsConnectedEvent( walletVersion: state.walletInfo?.version, commLayerVersion: packageJson.version, }, - state.communicationServerUrl, + state.analyticsServerUrl, ).catch((err) => { console.error(`Cannot send analytics`, err); }); diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts index 71c3ff7a5..22a6d767e 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/EventListeners/handleKeysExchangedEvent.ts @@ -81,7 +81,7 @@ export function handleKeysExchangedEvent( commLayerVersion: packageJson.version, walletVersion: state.walletInfo?.version, }, - state.communicationServerUrl, + state.analyticsServerUrl, ).catch((err) => { console.error(`Cannot send analytics`, err); }); diff --git a/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts b/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts index 000e93a9b..f91f84391 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/ConnectionManager/handleJoinChannelResult.ts @@ -114,7 +114,7 @@ export const handleJoinChannelResults = async ( commLayerVersion: packageJson.version, walletVersion: instance.remote.state.walletInfo?.version, }, - state.communicationServerUrl, + remote.state.analyticsServerUrl, ).catch((err) => { console.error(`Cannot send analytics`, err); }); diff --git a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts index cd9b4f367..145ff29a8 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleChannelRejected.ts @@ -43,7 +43,7 @@ export function handleChannelRejected( commLayerVersion: packageJson.version, walletVersion: instance.remote.state.walletInfo?.version, }, - instance.remote.state.communicationServerUrl, + instance.remote.state.analyticsServerUrl, ).catch((error) => { console.error( `handleChannelRejected:: Error emitting analytics event`, diff --git a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts index e04517012..bd46c4c5a 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/EventListeners/handleMessage.ts @@ -227,7 +227,7 @@ export function handleMessage(instance: SocketService, channelId: string) { from: 'mobile', }, }, - instance.remote.state.communicationServerUrl, + instance.remote.state.analyticsServerUrl, ).catch((err) => { console.error(`Cannot send analytics`, err); }); diff --git a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts index f8a4e0d50..b1c665459 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts @@ -83,7 +83,7 @@ export async function handleSendMessage( from: 'mobile', }, }, - instance.remote.state.communicationServerUrl, + instance.remote.state.analyticsServerUrl, ).catch((err) => { console.error(`[handleSendMessage] Cannot send analytics`, err); }); diff --git a/packages/sdk-socket-server-next/README.md b/packages/sdk-socket-server-next/README.md index 598671d98..6974174da 100644 --- a/packages/sdk-socket-server-next/README.md +++ b/packages/sdk-socket-server-next/README.md @@ -1,65 +1,101 @@ -# Debug SDK Socket Server Locally +# SDK Socket Server - Dockerized Development & Simulation Guide -This guide provides instructions for setting up and debugging the SDK socket server locally, as well as using Docker Compose for broader testing, including integration with MetaMask Mobile app. +This guide explains how to set up and run the SDK socket server using Docker Compose for different purposes: + +1. **Development Mode (Docker + Auto-Reload):** For coding and debugging within a Docker container, using auto-reloading code changes and integrated monitoring. +2. **Scalable Environment Simulation:** For testing the application in a multi-instance setup with load balancing, a Redis cluster, and integrated monitoring. ## Prerequisites -- Node.js and Yarn installed -- Docker and Docker Compose installed (for Docker-based setup) -- Ngrok account and CLI tool installed (for external access testing) +- Node.js and Yarn installed (for dependency management, though code runs in Docker) +- Docker and Docker Compose installed +- Ngrok account and CLI tool installed (optional, for external access testing) +- Copy `.env.sample` to `.env` and configure as needed (Note: `REDIS_NODES` in `.env` is ignored by Docker services, which use overrides in `docker-compose.yml`). + +## Mode 1: Development (Docker + Auto-Reload + Monitoring) + +This mode runs the development server (`yarn debug` via `nodemon`) _inside_ the `appdev` Docker container, which mounts your local code. It uses the `cache` Redis instance and integrates with Prometheus/Grafana. + +**Features:** + +- ✅ Automatic code reloading on file changes (via `appdev` service) +- ✅ Includes Prometheus/Grafana monitoring +- ✅ Runs app in a containerized environment (closer to production) -## QuickStart +**Setup & Run:** ```bash -# start local redis server -docker compose up -d cache -yarn debug -``` +# 1. Start background services (Redis, Prometheus, Grafana, Loki) +docker compose up -d cache prometheus grafana loki promtail -## Local Setup +# 2. Start the development application server in the foreground +# Logs will stream directly to your terminal. +# Use Ctrl+C to stop. +docker compose up appdev +``` -### Initial Configuration +- **Access Server:** `http://localhost:4000` +- **Access Prometheus:** `http://localhost:9090`. Check `Status` -> `Targets`. You should see the `appdev` job scraping `appdev:4000`. +- **Access Grafana:** `http://localhost:3444` (Login: `gadmin` / `admin`). Use the `Prometheus` datasource. +- **View Logs:** Logs stream directly when running `docker compose up appdev`. If you later run it with `-d`, use `docker compose logs -f appdev`. -1. **Set Up Environment Variables**: +## Mode 2: Scalable Environment Simulation (Docker Compose) - - Copy the sample environment file: `cp .env.sample .env` - - Adjust the `.env` file with the correct settings as per your project requirements. +This mode simulates a production-like deployment with multiple app instances (`app1`, `app2`, `app3`), Redis cluster, load balancer (`nginx`), and monitoring. -2. **Start the REDIS cluster**: - - For standard development, use: `yarn start` - - For debugging with more verbose output, use: `yarn debug` +**Features:** -3. **Check cluster status**: - - Use the command: `yarn docker:redis:check` - - This command sets up a local redis cluster and connect to it to make sure everything is working. +- ✅ Simulates horizontal scaling (`app1`, `app2`, `app3`) +- ✅ Includes load balancer (`nginx`) & Redis Cluster (`redis-master1..3`) +- ✅ Integrates Prometheus (scraping `app1..3`) & Grafana +- ❌ **NO** automatic code reloading for `app1..3` (requires image rebuild) +- ❌ Slower startup -4. **Start the SDK Socket Server via docker**: - - Use the command: `yarn docker:debug` +**Setup & Run:** -### Using Ngrok for External Access +```bash +# 1. (Optional) Build/Rebuild application images if code has changed +docker compose build app1 app2 app3 -To expose your local server to the internet, particularly for testing with mobile apps like MetaMask Mobile, use Ngrok. +# 2. Initialize Redis Cluster (if needed) +docker compose up redis-cluster-init -1. **Start Ngrok**: +# 3. Start all services for the scalable environment +docker compose up -d redis-master1 redis-master2 redis-master3 app1 app2 app3 nginx prometheus grafana loki promtail +``` - - Run the command: `ngrok http 4000` - - Note the generated https (and http) URL, which will be used in the MetaMask Mobile app settings. +- **Access Application:** Via Nginx load balancer at `http://localhost:8080`. +- **Access Prometheus:** `http://localhost:9090` (Check `Status` -> `Targets`. You should see `socket-server-scaled` job scraping `app1..3`. The `appdev` target will likely be DOWN unless you explicitly started it). +- **Access Grafana:** `http://localhost:3444` (Login: `gadmin` / `admin`). -2. **Configure MetaMask Mobile App**: +**Deploying Code Changes in Mode 2:** +Requires image rebuild and container restart: - - Set `MM_SDK.SERVER_URL` in the MetaMask app to the https URL provided by Ngrok. +1. `docker compose stop app1 app2 app3` +2. `docker compose build app1 app2 app3` +3. `docker compose up -d --force-recreate app1 app2 app3` -3. **Configure Your DApp**: - - Set the `communicationServerUrl` in your DApp's SDK options to your local IP or `localhost` with port 4000. For example: `communicationServerUrl: "http://{yourLocalIP | localhost}:4000"` +## Using Ngrok for External Access -### Ngrok Configuration +If you need to expose either the development server (`Mode 1`) or the Dockerized load balancer (`Mode 2`) to the internet: -Follow the same Ngrok setup as mentioned in the Local Setup section above to expose your Docker Compose-based server. +1. **Identify the Port:** + - Mode 1 (`appdev`): `4000` + - Mode 2 (Nginx): `8080` +2. **Start Ngrok:** + ```bash + # For Mode 1 + ngrok http 4000 + # For Mode 2 + ngrok http 8080 + ``` +3. Note the generated `https` URL from Ngrok. +4. **Configure MetaMask Mobile:** Set `MM_SDK.SERVER_URL` in the app to the Ngrok `https` URL. +5. **Configure Your DApp (if applicable):** Ensure your DApp points to the correct server URL. ## Additional Notes -- **Environment-Specific Configuration**: The development mode includes additional debugging tools and settings, while the production mode is streamlined for performance. -- **Redis Setup**: Ensure that Redis is properly configured and running when using Docker Compose. -- **Logs and Monitoring**: Monitor the logs for any error messages or warnings during startup or operation of the server. -- **Security Considerations**: When using Ngrok, be aware that your server is publicly accessible. Ensure that you do not expose sensitive data or endpoints. -- **Troubleshooting**: If you encounter issues, verify your Docker Compose and Ngrok configurations. Check for network connectivity issues and ensure that all containers are running as expected. +- **Environment Variables**: Other variables from `.env` are still loaded by services with `env_file: - .env`. +- **Redis Data**: Redis data is persisted in Docker volumes. Use `docker compose down -v` to remove data volumes. +- **Logs**: Check container logs using `docker compose logs ` (e.g., `docker compose logs app1`). +- **Security**: Be cautious when exposing services via Ngrok. diff --git a/packages/sdk-socket-server-next/docker-compose.yml b/packages/sdk-socket-server-next/docker-compose.yml index ee9927d69..0e3fcdd13 100644 --- a/packages/sdk-socket-server-next/docker-compose.yml +++ b/packages/sdk-socket-server-next/docker-compose.yml @@ -7,6 +7,11 @@ services: - ./:/usr/src/app working_dir: /usr/src/app command: yarn debug:redis + labels: + - "logging=promtail" + - "service=check-redis" + - "job=debug" + - "env=development" appdev: image: node:latest @@ -16,45 +21,88 @@ services: command: yarn debug ports: - '4000:4000' + environment: + - REDIS_NODES=redis://cache:6379 + - NODE_ENV=development + depends_on: + - cache + labels: + - "logging=promtail" + - "service=appdev" + - "job=socket-server-dev" + - "env=development" app1: build: - context: . - dockerfile: Dockerfile - args: - - NODE_ENV=${NODE_ENV:-production} + context: ../../ + dockerfile: ./packages/sdk-socket-server-next/Dockerfile ports: - '4002:4000' env_file: - .env depends_on: - - cache + - redis-master1 + - redis-master2 + - redis-master3 + environment: + - REDIS_NODES=redis://redis-master1:6379,redis://redis-master2:6379,redis://redis-master3:6379 + - REDIS_CLUSTER=true + - NODE_ENV=development + - PORT=9000 + - SENTRY_DSN= + labels: + - "logging=promtail" + - "service=app1" + - "job=socket-server-scaled" + - "env=production" app2: build: - context: . - dockerfile: Dockerfile - args: - - NODE_ENV=${NODE_ENV:-production} + context: ../../ + dockerfile: ./packages/sdk-socket-server-next/Dockerfile ports: - '4003:4000' env_file: - .env depends_on: - - cache + - redis-master1 + - redis-master2 + - redis-master3 + environment: + - REDIS_NODES=redis://redis-master1:6379,redis://redis-master2:6379,redis://redis-master3:6379 + - REDIS_CLUSTER=true + - NODE_ENV=development + - PORT=9000 + - SENTRY_DSN= + labels: + - "logging=promtail" + - "service=app2" + - "job=socket-server-scaled" + - "env=production" app3: build: - context: . - dockerfile: Dockerfile - args: - - NODE_ENV=${NODE_ENV:-production} + context: ../../ + dockerfile: ./packages/sdk-socket-server-next/Dockerfile ports: - '4004:4000' env_file: - .env depends_on: - - cache + - redis-master1 + - redis-master2 + - redis-master3 + environment: + - REDIS_NODES=redis://redis-master1:6379,redis://redis-master2:6379,redis://redis-master3:6379 + - REDIS_CLUSTER=true + - NODE_ENV=development + - PORT=9000 + - SENTRY_DSN= + labels: + - "logging=promtail" + - "service=app3" + - "job=socket-server-scaled" + - "env=production" redis-master1: image: redis:7.2-alpine @@ -63,6 +111,11 @@ services: - REDIS_ROLE=master ports: - "6380:6379" + labels: + - "logging=promtail" + - "service=redis-master1" + - "job=redis-cluster" + - "env=production" redis-master2: image: redis:7.2-alpine @@ -71,6 +124,11 @@ services: - REDIS_ROLE=master ports: - "6381:6379" + labels: + - "logging=promtail" + - "service=redis-master2" + - "job=redis-cluster" + - "env=production" redis-master3: image: redis:7.2-alpine @@ -79,6 +137,11 @@ services: - REDIS_ROLE=master ports: - "6382:6379" + labels: + - "logging=promtail" + - "service=redis-master3" + - "job=redis-cluster" + - "env=production" redis-cluster-init: image: redis:7.2-alpine @@ -96,6 +159,11 @@ services: command: redis-server --maxmemory 100mb --maxmemory-policy volatile-lru --loglevel debug ports: - "${DOCKER_ENV_LOCAL_REDIS_PORT:-6379}:6379" + labels: + - "logging=promtail" + - "service=cache" + - "job=redis-dev" + - "env=development" nginx: image: nginx:latest @@ -107,3 +175,72 @@ services: - app1 - app2 - app3 + + prometheus: + image: prom/prometheus:v2.47.2 # Pinned version for stability + volumes: + # Mount the configuration file + - ./prometheus.yml:/etc/prometheus/prometheus.yml + # Mount a named volume for persistent Prometheus data + - prometheus_data:/prometheus + command: + # Standard Prometheus startup command with config file and storage path + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/usr/share/prometheus/console_libraries' + - '--web.console.templates=/usr/share/prometheus/consoles' + - '--web.enable-lifecycle' # Allows config reload via API + ports: + # Expose Prometheus UI on host port 9090 + - '9090:9090' + restart: unless-stopped # Optional: ensures Prometheus restarts if it stops unexpectedly + + grafana: + image: grafana/grafana:10.1.5 # Pinned version for stability + ports: + # Expose Grafana UI on host port 3444 + - '3444:3000' + volumes: + # Mount a named volume for persistent Grafana data (dashboards, etc.) + - grafana_data:/var/lib/grafana + # Mount provisioning configuration (datasources and dashboards) + - ./grafana/provisioning:/etc/grafana/provisioning + # Mount dashboard definition files (used by dashboard provisioning) + - ./grafana/dashboards:/var/lib/grafana/dashboards/json + environment: + # Use environment variables for credentials, fallback to defaults + - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-gadmin} + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin} + depends_on: + # Ensure Prometheus and Loki are running before Grafana starts + - prometheus + - loki + restart: unless-stopped # Optional: ensures Grafana restarts if it stops unexpectedly + + loki: + image: grafana/loki:2.9.0 + ports: + - "3100:3100" + volumes: + - ./loki-config.yaml:/etc/loki/local-config.yaml + - loki_data:/loki + command: -config.file=/etc/loki/local-config.yaml + restart: unless-stopped + + promtail: + image: grafana/promtail:2.9.0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./promtail-config.yaml:/etc/promtail/config.yaml + command: -config.file=/etc/promtail/config.yaml + depends_on: + - loki + restart: unless-stopped + +# Define named volumes for persistent storage +# Data stored here will survive container removal (docker compose down) +# Use `docker compose down -v` to remove the volumes as well +volumes: + grafana_data: {} + prometheus_data: {} + loki_data: {} diff --git a/packages/sdk-socket-server-next/grafana/dashboards/basic.json b/packages/sdk-socket-server-next/grafana/dashboards/basic.json new file mode 100644 index 000000000..a490153eb --- /dev/null +++ b/packages/sdk-socket-server-next/grafana/dashboards/basic.json @@ -0,0 +1,216 @@ +{ + "__inputs": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.1.5" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "socket_io_server_total_clients", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Connected Clients", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 0 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(analytics_requests_total[5m])) by (status)", + "legendFormat": "status={{status}}", + "range": true, + "refId": "A" + } + ], + "title": "Analytics Request Rate (by status)", + "type": "timeseries" + } + ], + "refresh": "10s", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Basic Socket Server Metrics", + "uid": "basic-socket-server", + "version": 1, + "weekStart": "" +} diff --git a/packages/sdk-socket-server-next/grafana/dashboards/logs-dashboard.json b/packages/sdk-socket-server-next/grafana/dashboards/logs-dashboard.json new file mode 100644 index 000000000..a30218bba --- /dev/null +++ b/packages/sdk-socket-server-next/grafana/dashboards/logs-dashboard.json @@ -0,0 +1,95 @@ +{ + "__inputs": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.1.5" + }, + { + "type": "datasource", + "id": "loki", + "name": "Loki", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "sS52nlJVz" + }, + "gridPos": { + "h": 20, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": true, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "sS52nlJVz" + }, + "editorMode": "code", + "expr": "{job=\"socket-server-dev\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs (socket-server-dev)", + "type": "logs" + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": ["logs", "loki"], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Container Logs", + "uid": "container-logs-dashboard", + "version": 1, + "weekStart": "" +} diff --git a/packages/sdk-socket-server-next/grafana/dashboards/relay-server.json b/packages/sdk-socket-server-next/grafana/dashboards/relay-server.json new file mode 100644 index 000000000..d31e966d3 --- /dev/null +++ b/packages/sdk-socket-server-next/grafana/dashboards/relay-server.json @@ -0,0 +1,3589 @@ +{ + "__inputs": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.1.5" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "datasource", + "id": "loki", + "name": "Loki", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 36, + "panels": [], + "title": "Socket handlers", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 44, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum(rate(join_channel_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - join_channel_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 45, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(join_channel_error_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - join_channel_error_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurationms" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max by(quantile) (join_channel_duration_milliseconds)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - join_channel_duration_milliseconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 6 + }, + "id": 38, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(message_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - message_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 6 + }, + "id": 39, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(message_error_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - message_error_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurationms" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 6 + }, + "id": 46, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max by(quantile) (message_duration_milliseconds)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - message_duration_milliseconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 11 + }, + "id": 42, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(ping_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - ping_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 11 + }, + "id": 43, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(ping_error_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - ping_error_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurationms" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 11 + }, + "id": 47, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max by(quantile) (ping_duration_milliseconds)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - ping_duration_milliseconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 37, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(ack_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - ack_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 41, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(ack_error_total[$__rate_interval]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - ack_error_total/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurationms" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 48, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max by(quantile) (ack_duration_milliseconds)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - ack_duration_milliseconds", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 14, + "panels": [], + "title": "Socket", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 9, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(socket_io_server_total_clients)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "socket_io_server_total_clients", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "legendFormat": "{{pod}}", + "range": true, + "refId": "B", + "useBackend": false + } + ], + "title": "Current Connected Clients", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 10, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(socket_io_server_total_rooms)", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "total", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Current Active Rooms", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "sS52nlJVz" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "sS52nlJVz" + }, + "editorMode": "builder", + "expr": "sum(rate({job=\"socket-server-dev\"} |~ \"warn|error\" [$__interval]))", + "hide": false, + "legendFormat": "warn/error rate", + "queryType": "range", + "refId": "B" + } + ], + "title": "Error Logs Volumes", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 13, + "panels": [], + "title": "Pod", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": ["max"], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 32 + }, + "id": 8, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "(max(rate(container_cpu_usage_seconds_total[5m])) * 100) / max(kube_pod_container_resource_limits{resource=\"cpu\"})", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "(sum by(pod) (rate(container_cpu_usage_seconds_total[5m])) * 100) / sum by(pod) (kube_pod_container_resource_limits{resource=\"cpu\"})", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 32 + }, + "id": 17, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "(max(container_memory_working_set_bytes) * 100) / max(kube_pod_container_resource_limits)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "(sum by(pod) (container_memory_working_set_bytes) * 100) / sum by(pod) (kube_pod_container_resource_limits)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "barWidthFactor": 0.5, + "drawStyle": "line", + "fillOpacity": 36, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 32 + }, + "id": 12, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max(kube_pod_container_resource_limits)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "total", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max(container_memory_working_set_bytes)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "used", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 40 + }, + "id": 20, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(process_cpu_seconds_total[5m]) * 100", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{pod}}", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "CPU Usage (node metrics)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "decmbytes" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": ["max"], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 40 + }, + "id": 21, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "process_resident_memory_bytes / 1024 / 1024", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Memory Usage (node metrics)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 40 + }, + "id": 22, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(kube_pod_container_status_running)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "total", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Pods Running", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 48 + }, + "id": 23, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "max(nodejs_eventloop_lag_seconds)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(pod) (nodejs_eventloop_lag_seconds)", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Event Loop Lag", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 56 + }, + "id": 24, + "panels": [], + "title": "Analytics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 57 + }, + "id": 25, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(analytics_requests_total[$__rate_interval]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "total", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(analytics_requests_total[$__rate_interval])", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 57 + }, + "id": 26, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(platform) (increase(analytics_events_total[$__rate_interval]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Events by Platform", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 57 + }, + "id": 27, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "histogram_quantile(0.95, sum by(le) (rate(analytics_request_duration_seconds_bucket[$__rate_interval])))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "95th percentile", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Request Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 65 + }, + "id": 28, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(sdk_version) (increase(analytics_events_total[$__rate_interval]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Events by SDK Version", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 65 + }, + "id": 29, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(event_name) (increase(analytics_events_total[5m]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Events by Name", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 65 + }, + "id": 30, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(error_type) (increase(analytics_errors_total[5m]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Errors by Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 73 + }, + "id": 31, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(from) (increase(analytics_events_total[$__rate_interval]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Events by Source", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 73 + }, + "id": 32, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(operation) (increase(redis_cache_operations_total[5m]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Redis Cache Operations", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 73 + }, + "id": 33, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum by(result) (increase(redis_cache_operations_total[5m]))", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Redis Cache Operations Result", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 81 + }, + "id": 34, + "panels": [], + "title": "Redis", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 82 + }, + "id": 35, + "options": { + "legend": { + "calcs": ["mean"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "redis_memory_max_bytes{namespace=\"metamask-sdk\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "max", + "range": true, + "refId": "C", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "redis_memory_used_bytes{namespace=\"metamask-sdk\"}", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "used", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Handler - check_room_duration_milliseconds", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 14, + "panels": [], + "title": "Socket", + "type": "row" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 41, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "prd", + "value": "prd" + }, + "includeAll": false, + "name": "env", + "options": [ + { + "selected": false, + "text": "dev", + "value": "dev" + }, + { + "selected": true, + "text": "prd", + "value": "prd" + } + ], + "query": "dev, prd", + "type": "custom" + }, + { + "current": { + "text": "metamask-sdk-multinode", + "value": "metamask-sdk-multinode" + }, + "name": "version", + "options": [ + { + "selected": false, + "text": "v1", + "value": "metamask-sdk" + }, + { + "selected": true, + "text": "v2", + "value": "metamask-sdk-multinode" + } + ], + "query": "v1 : metamask-sdk, v2 : metamask-sdk-multinode", + "type": "custom" + } + ] + }, + "time": { + "from": "now-2d", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "SDK Relay Server", + "uid": "ee1j0663mp6o0a", + "version": 151, + "weekStart": "" +} diff --git a/packages/sdk-socket-server-next/grafana/provisioning/dashboards/default.yaml b/packages/sdk-socket-server-next/grafana/provisioning/dashboards/default.yaml new file mode 100644 index 000000000..061375057 --- /dev/null +++ b/packages/sdk-socket-server-next/grafana/provisioning/dashboards/default.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'default' + orgId: 1 + folder: '' # Imports dashboards into the main folder + type: file + disableDeletion: false # Allows dashboards to be deleted from the UI + editable: true # Allows dashboards to be edited from the UI + options: + path: /var/lib/grafana/dashboards/json # Path inside the container where dashboard JSONs are mounted \ No newline at end of file diff --git a/packages/sdk-socket-server-next/grafana/provisioning/datasources/loki.yaml b/packages/sdk-socket-server-next/grafana/provisioning/datasources/loki.yaml new file mode 100644 index 000000000..4cd59fa13 --- /dev/null +++ b/packages/sdk-socket-server-next/grafana/provisioning/datasources/loki.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + jsonData: + maxLines: 1000 + uid: sS52nlJVz # Explicit UID to match dashboard + editable: false \ No newline at end of file diff --git a/packages/sdk-socket-server-next/grafana/provisioning/datasources/prometheus.yaml b/packages/sdk-socket-server-next/grafana/provisioning/datasources/prometheus.yaml new file mode 100644 index 000000000..d134547ea --- /dev/null +++ b/packages/sdk-socket-server-next/grafana/provisioning/datasources/prometheus.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus # Name of the datasource + type: prometheus # Type of the datasource + access: proxy # How Grafana accesses the datasource (proxy or direct) + url: http://prometheus:9090 # URL of the Prometheus server (using the service name) + isDefault: true # Make this the default datasource + editable: false # Prevent users from editing this datasource in the UI \ No newline at end of file diff --git a/packages/sdk-socket-server-next/loki-config.yaml b/packages/sdk-socket-server-next/loki-config.yaml new file mode 100644 index 000000000..7ca7c9ecc --- /dev/null +++ b/packages/sdk-socket-server-next/loki-config.yaml @@ -0,0 +1,38 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + +common: + instance_addr: 127.0.0.1 + path_prefix: /tmp/loki + storage: + filesystem: + chunks_directory: /tmp/loki/chunks + rules_directory: /tmp/loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: boltdb-shipper + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 + +# By default, Loki will send anonymous, but uniquely-identifiable usage statistics to +# Grafana Labs. Collecting statistics is fairly standard across open-source software +# projects. It helps us improve Loki. +# For more information, and how to disable it, see +# https://grafana.com/docs/loki/latest/configuration/#analytics +analytics: + reporting_enabled: false \ No newline at end of file diff --git a/packages/sdk-socket-server-next/package.json b/packages/sdk-socket-server-next/package.json index b5c8b77c2..d886a9dcd 100644 --- a/packages/sdk-socket-server-next/package.json +++ b/packages/sdk-socket-server-next/package.json @@ -19,6 +19,7 @@ "build:pre-tsc": "echo 'N/A'", "typecheck": "tsc --noEmit", "clean": "rimraf dist", + "clean:redis": "docker compose down -v && docker compose up -d cache", "docker:redis": "docker compose up redis-cluster-init", "docker:redis:check": "yarn docker:redis && docker compose up check-redis", "debug": "nodemon --exec 'NODE_ENV=development ts-node --transpile-only src/index.ts'", @@ -49,14 +50,14 @@ "express-rate-limit": "^7.1.5", "generic-pool": "^3.9.0", "helmet": "^5.1.1", - "ioredis": "^5.3.2", + "ioredis": "^5.6.0", "logform": "^2.6.0", "lru-cache": "^10.0.0", "prom-client": "^15.1.3", "rate-limiter-flexible": "^2.3.8", "redis": "^4.6.12", "rimraf": "^4.4.0", - "socket.io": "^4.4.1", + "socket.io": "^4.7.2", "uuid": "^9.0.1", "winston": "^3.11.0", "winston-loki": "^6.0.8" diff --git a/packages/sdk-socket-server-next/prometheus.yml b/packages/sdk-socket-server-next/prometheus.yml new file mode 100644 index 000000000..8cfe0cbc8 --- /dev/null +++ b/packages/sdk-socket-server-next/prometheus.yml @@ -0,0 +1,16 @@ +# Global Prometheus configuration +global: + scrape_interval: 15s # How often to scrape targets + evaluation_interval: 15s # How often to evaluate alerting rules (if any) + +# Scrape configurations: Defines targets to monitor +scrape_configs: + # Job for scraping the scaled application instances (Mode 2) + - job_name: 'socket-server-scaled' + static_configs: + - targets: ['app1:4000', 'app2:4000', 'app3:4000'] + + # Job for scraping the containerized development instance (Mode 1) + - job_name: 'appdev' + static_configs: + - targets: ['appdev:4000'] # Target the 'appdev' service name on its internal port \ No newline at end of file diff --git a/packages/sdk-socket-server-next/promtail-config.yaml b/packages/sdk-socket-server-next/promtail-config.yaml new file mode 100644 index 000000000..964042b94 --- /dev/null +++ b/packages/sdk-socket-server-next/promtail-config.yaml @@ -0,0 +1,30 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: +- job_name: containers + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + filters: + - name: label + values: ["logging=promtail"] + relabel_configs: + - source_labels: ['__meta_docker_container_id'] + target_label: 'container_id' + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container_name' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - source_labels: ['__meta_docker_container_label_job'] + target_label: 'job' + - source_labels: ['__meta_docker_container_label_env'] + target_label: 'env' \ No newline at end of file diff --git a/packages/sdk-socket-server-next/src/analytics-api.ts b/packages/sdk-socket-server-next/src/analytics-api.ts deleted file mode 100644 index 1ee2e3e17..000000000 --- a/packages/sdk-socket-server-next/src/analytics-api.ts +++ /dev/null @@ -1,467 +0,0 @@ -/* eslint-disable node/no-process-env */ -import crypto from 'crypto'; -import Analytics from 'analytics-node'; -import bodyParser from 'body-parser'; -import cors from 'cors'; -import express from 'express'; -import { rateLimit } from 'express-rate-limit'; -import helmet from 'helmet'; -import { Cluster, ClusterOptions, Redis, RedisOptions } from 'ioredis'; -import { - config, - EVENTS_DEBUG_LOGS, - hasRateLimit, - isDevelopment, - isDevelopmentServer, - REDIS_DEBUG_LOGS, - redisCluster, - redisTLS, -} from './config'; -import { getLogger } from './logger'; -import { ChannelInfo, extractChannelInfo } from './utils'; -import { evtMetricsMiddleware } from './middleware-metrics'; -import { - incrementAnalyticsError, - incrementAnalyticsEvents, - incrementRedisCacheOperation, -} from './metrics'; -import genericPool from "generic-pool"; - -const logger = getLogger(); - -// SDK version prev 0.27.0 uses 'sdk' as the default id, below value is the sha1 hash of 'sdk' -const SDK_EXTENSION_DEFAULT_ID = '5a374dcd2e5eb762b527af3a5bab6072a4d24493'; - -// Initialize Redis Cluster client -let redisNodes: { - host: string; - port: number; -}[] = []; - -if (process.env.REDIS_NODES) { - // format: REDIS_NODES=redis://rediscluster-redis-cluster-0.rediscluster-redis-cluster-headless.redis.svc.cluster.local:6379,redis://rediscluster-redis-cluster-1.rediscluster-redis-cluster-headless.redis.svc.cluster.local:6379,redis://rediscluster-redis-cluster-2.rediscluster-redis-cluster-headless.redis.svc.cluster.local:6379 - redisNodes = process.env.REDIS_NODES.split(',').map((node) => { - const [host, port] = node.replace('redis://', '').split(':'); - return { - host, - port: parseInt(port, 10), - }; - }); -} -logger.info('Redis nodes:', redisNodes); - -if (redisNodes.length === 0) { - logger.error('No Redis nodes found'); - process.exit(1); -} - -export const getRedisOptions = ( - isTls: boolean, - password: string | undefined, -): RedisOptions => { - const tlsOptions = { - tls: { - checkServerIdentity: (/* host, cert*/) => { - return undefined; - }, - }, - }; - - const options: RedisOptions = { - ...(isTls && tlsOptions), - connectTimeout: 30000, - keepAlive: 369, - maxRetriesPerRequest: 4, - retryStrategy: (times) => Math.min(times * 30, 1000), - reconnectOnError: (error) => { - // eslint-disable-next-line require-unicode-regexp - const targetErrors = [/MOVED/, /READONLY/, /ETIMEDOUT/]; - - logger.error('Redis reconnect error:', error); - return targetErrors.some((targetError) => - targetError.test(error.message), - ); - }, - }; - if (password) { - options.password = password; - } - return options; -}; - -export const buildRedisClient = (usePipelining: boolean = true) => { - let newRedisClient: Cluster | Redis | undefined; - - if (redisCluster) { - logger.info('Connecting to Redis Cluster...'); - - const redisOptions = getRedisOptions( - redisTLS, - process.env.REDIS_PASSWORD, - ); - const redisClusterOptions: ClusterOptions = { - dnsLookup: (address, callback) => callback(null, address), - scaleReads: 'slave', - slotsRefreshTimeout: 5000, - showFriendlyErrorStack: true, - slotsRefreshInterval: 2000, - clusterRetryStrategy: (times) => Math.min(times * 30, 1000), - enableAutoPipelining: usePipelining, - redisOptions, - }; - - logger.debug( - 'Redis Cluster options:', - JSON.stringify(redisClusterOptions, null, 2), - ); - - newRedisClient = new Cluster(redisNodes, redisClusterOptions); - } else { - logger.info('Connecting to single Redis node'); - newRedisClient = new Redis(redisNodes[0]); - } - - newRedisClient.on('ready', () => { - logger.info('Redis ready'); - }); - - newRedisClient.on('error', (error) => { - logger.error('Redis error:', error); - }); - - newRedisClient.on('connect', () => { - logger.info('Connected to Redis Cluster successfully'); - }); - - newRedisClient.on('close', () => { - logger.info('Disconnected from Redis Cluster'); - }); - - newRedisClient.on('reconnecting', () => { - logger.info('Reconnecting to Redis Cluster'); - }); - - newRedisClient.on('end', () => { - logger.info('Redis Cluster connection ended'); - }); - - newRedisClient.on('wait', () => { - logger.info('Redis Cluster waiting for connection'); - }); - - newRedisClient.on('select', (node) => { - logger.info('Redis Cluster selected node:', node); - }); - - return newRedisClient; -} - -const redisFactory = { - create: () => { - return Promise.resolve(buildRedisClient(false)); - }, - destroy: (client: Cluster | Redis) => { - return Promise.resolve(client.disconnect()); - }, -}; - -let redisClient: Cluster | Redis | undefined; - -export const getGlobalRedisClient = () => { - if (!redisClient) { - redisClient = buildRedisClient(); - } - - return redisClient; -}; - -export const pubClient = getGlobalRedisClient(); -export const pubClientPool = genericPool.createPool(redisFactory, { - max: 35, - min: 15, -}); - -const app = express(); - -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); -app.use(cors()); -app.options('*', cors()); -app.use(helmet()); -app.disable('x-powered-by'); - -if (hasRateLimit) { - // Conditionally apply the rate limiting middleware to all requests. - let windowMin = 1; // every 1minute - try { - if (process.env.RATE_LIMITER_HTTP_LIMIT) { - windowMin = parseInt( - process.env.RATE_LIMITER_HTTP_WINDOW_MINUTE ?? '1', - 10, - ); - } - } catch (error) { - // Ignore parsing errors - } - let limit = 100_000; // 100,000 requests per minute by default (unlimited...) - try { - if (process.env.RATE_LIMITER_HTTP_LIMIT) { - limit = parseInt(process.env.RATE_LIMITER_HTTP_LIMIT, 10); - } - } catch (error) { - // Ignore parsing errors - } - - const limiterConfig = { - windowMs: windowMin * 60 * 1000, - limit, - legacyHeaders: false, // Disable the `X-RateLimit-*` headers. - // store: ... , // Use an external store for consistency across multiple server instances. - }; - const limiter = rateLimit(limiterConfig); - - logger.info('Rate limiter enabled', limiterConfig); - app.use(limiter); -} - -async function inspectRedis(key?: string) { - if (key && typeof key === 'string') { - const value = await pubClient.get(key); - logger.debug(`inspectRedis Key: ${key}, Value: ${value}`); - } -} - -const analytics = new Analytics( - isDevelopment || isDevelopmentServer - ? process.env.SEGMENT_API_KEY_DEBUG || '' - : process.env.SEGMENT_API_KEY_PRODUCTION || '', - { - flushInterval: isDevelopment ? 1000 : 10000, - errorHandler: (err: Error) => { - logger.error(`ERROR> Analytics-node flush failed: ${err}`); - }, - }, -); - -app.get('/', (req, res) => { - if (process.env.NODE_ENV === 'development') { - logger.info(`health check from`, { - 'x-forwarded-for': req.headers['x-forwarded-for'], - 'cf-connecting-ip': req.headers['cf-connecting-ip'], - }); - } - - res.json({ success: true }); -}); - -// Redirect /debug to /evt for backwards compatibility -app.post('/debug', (req, _res, next) => { - req.url = '/evt'; // Redirect to /evt - next(); // Pass control to the next handler (which will be /evt) -}); - -app.post('/evt', evtMetricsMiddleware, async (_req, res) => { - try { - const { body } = _req; - - if (!body.event) { - logger.error(`Event is required`); - incrementAnalyticsError('MissingEventError'); - return res.status(400).json({ error: 'event is required' }); - } - - if (!body.event.startsWith('sdk_')) { - logger.error(`Wrong event name: ${body.event}`); - incrementAnalyticsError('WrongEventNameError'); - return res.status(400).json({ error: 'wrong event name' }); - } - - const toCheckEvents = ['sdk_rpc_request_done', 'sdk_rpc_request']; - const allowedMethods = [ - "eth_sendTransaction", - "wallet_switchEthereumChain", - "personal_sign", - "eth_signTypedData_v4", - "wallet_requestPermissions", - "metamask_connectSign" - ]; - - // Filter: drop RPC events with unallowed methods silently, let all else through - if (toCheckEvents.includes(body.event) && - (!body.method || !allowedMethods.includes(body.method))) { - return res.json({ success: true }); - } - - let channelId: string = body.id || 'sdk'; - // Prevent caching of events coming from extension since they are not re-using the same id and prevent increasing redis queue size. - let isExtensionEvent = body.from === 'extension'; - - if (typeof channelId !== 'string') { - logger.error(`Received event with invalid channelId: ${channelId}`, body); - incrementAnalyticsError('InvalidChannelIdError'); - return res.status(400).json({ status: 'error' }); - } - - let isAnonUser = false; - - if (channelId === 'sdk') { - isAnonUser = true; - isExtensionEvent = true; - } - - logger.debug( - `Received event /evt channelId=${channelId} isExtensionEvent=${isExtensionEvent}`, - body, - ); - - let userIdHash = isAnonUser - ? crypto.createHash('sha1').update(channelId).digest('hex') - : await pubClient.get(channelId); - - incrementRedisCacheOperation('analytics-get-channel-id', !!userIdHash); - - if (!userIdHash) { - userIdHash = crypto.createHash('sha1').update(channelId).digest('hex'); - logger.info( - `event: ${body.event} channelId: ${channelId} - No cached channel info found for ${userIdHash} - creating new channelId`, - ); - - if (!isExtensionEvent) { - await pubClient.set( - channelId, - userIdHash, - 'EX', - config.channelExpiry.toString(), - ); - } - } - - if (REDIS_DEBUG_LOGS) { - await inspectRedis(channelId); - } - - let channelInfo: ChannelInfo | null; - const cachedChannelInfo = isAnonUser - ? null - : await pubClient.get(userIdHash); - - incrementRedisCacheOperation( - 'analytics-get-channel-info', - !!cachedChannelInfo, - ); - - if (cachedChannelInfo) { - logger.debug( - `Found cached channel info for ${userIdHash}`, - cachedChannelInfo, - ); - channelInfo = JSON.parse(cachedChannelInfo); - } else { - logger.info( - `event: ${body.event} channelId: ${channelId} - No cached channel info found for ${userIdHash}`, - ); - - // Extract channelInfo from any events if available - channelInfo = extractChannelInfo(body); - - if (!channelInfo) { - logger.info( - `event: ${body.event} channelId: ${channelId} - Invalid channelInfo format - event will be ignored`, - JSON.stringify(body, null, 2), - ); - // always return success - return res.json({ success: true }); - } - - // Save the channelInfo in Redis - logger.info( - `Adding channelInfo for event=${body.event} channelId=${channelId} userIdHash=${userIdHash} expiry=${config.channelExpiry}`, - channelInfo, - ); - - if (!isExtensionEvent) { - await pubClient.set( - userIdHash, - JSON.stringify(channelInfo), - 'EX', - config.channelExpiry.toString(), - ); - } - } - - if (REDIS_DEBUG_LOGS) { - await inspectRedis(userIdHash); - } - - const event = { - userId: userIdHash, - event: body.event, - properties: { - userId: userIdHash, - ...body.properties, - // Apply channelInfo properties - ...channelInfo, - }, - }; - - if (!event.properties.dappId) { - // Prevent "N/A" in url and ensure a valid dappId - const newDappId = - event.properties.url && event.properties.url !== 'N/A' - ? event.properties.url - : event.properties.title || 'N/A'; - event.properties.dappId = newDappId; - logger.debug( - `event: ${event.event} - dappId missing - replacing with '${newDappId}'`, - event, - ); - } - - // Define properties to be excluded - const propertiesToExclude: string[] = ['icon', 'originationInfo', 'id']; - - for (const property in body) { - if ( - Object.prototype.hasOwnProperty.call(body, property) && - body[property] && - !propertiesToExclude.includes(property) - ) { - event.properties[property] = body[property]; - } - } - - if (EVENTS_DEBUG_LOGS) { - logger.debug('Event object:', event); - } - - incrementAnalyticsEvents( - body.from, - !isAnonUser, - event.event, - body.platform, - body.sdkVersion, - ); - - analytics.track(event, function (err: Error) { - if (EVENTS_DEBUG_LOGS) { - logger.info('Segment batch', JSON.stringify({ event }, null, 2)); - } else { - logger.info('Segment batch', { event }); - } - - if (err) { - incrementAnalyticsError('SegmentError'); - logger.error('Segment error:', err); - } - }); - - return res.json({ success: true }); - } catch (error) { - incrementAnalyticsError( - error instanceof Error ? error.constructor.name : 'UnknownError', - ); - return res.json({ error }); - } -}); - -export { analytics, app }; diff --git a/packages/sdk-socket-server-next/src/app.ts b/packages/sdk-socket-server-next/src/app.ts new file mode 100644 index 000000000..cbcb7ebd2 --- /dev/null +++ b/packages/sdk-socket-server-next/src/app.ts @@ -0,0 +1,77 @@ +import bodyParser from 'body-parser'; +import cors from 'cors'; +import express from 'express'; +import { rateLimit } from 'express-rate-limit'; +import helmet from 'helmet'; +import packageJson from '../package.json'; +import { hasRateLimit } from './config'; +import { getLogger } from './logger'; +import { analyticsRedirectMiddleware } from './middleware-analytics-redirect'; +import { readMetrics } from './metrics'; + +const logger = getLogger(); + +const app = express(); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); +app.use(cors()); +app.options('*', cors()); +app.use(helmet()); +app.disable('x-powered-by'); + +if (hasRateLimit) { + // Conditionally apply the rate limiting middleware to all requests. + let windowMin = 1; // every 1minute + try { + if (process.env.RATE_LIMITER_HTTP_WINDOW_MINUTE) { + windowMin = parseInt(process.env.RATE_LIMITER_HTTP_WINDOW_MINUTE, 10); + } + } catch (error) { + logger.error('Error parsing RATE_LIMITER_HTTP_WINDOW_MINUTE', error); + // Ignore parsing errors, default to 1 min + } + + let limit = 100_000; // 100,000 requests per minute by default (effectively unlimited) + try { + if (process.env.RATE_LIMITER_HTTP_LIMIT) { + limit = parseInt(process.env.RATE_LIMITER_HTTP_LIMIT, 10); + } + } catch (error) { + logger.error('Error parsing RATE_LIMITER_HTTP_LIMIT', error); + // Ignore parsing errors, default to 100k + } + + const limiterConfig = { + windowMs: windowMin * 60 * 1000, + limit, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + // store: ... , // Use an external store for consistency across multiple server instances. + }; + const limiter = rateLimit(limiterConfig); + + logger.info('Rate limiter enabled', limiterConfig); + app.use(limiter); +} + +// Basic Routes (moved from index.ts) +// Make sure to protect the endpoint to be only available within the cluster for prometheus +app.get('/metrics', async (_req, res) => { + res.set('Content-Type', 'text/plain'); + res.send(await readMetrics()); +}); + +app.get('/version', (_req, res) => { + res.send({ version: packageJson.version }); +}); + +// Health check moved from analytics-api.ts (now handled by redirect middleware effectively) +app.get('/', (_req, res) => { + res.json({ success: true, message: 'Socket server is running' }); +}); + +// Analytics Redirect Middleware +app.use(analyticsRedirectMiddleware); + +export { app }; diff --git a/packages/sdk-socket-server-next/src/config.ts b/packages/sdk-socket-server-next/src/config.ts index 7acb5dad1..9dc87145c 100644 --- a/packages/sdk-socket-server-next/src/config.ts +++ b/packages/sdk-socket-server-next/src/config.ts @@ -53,3 +53,6 @@ if (process.env.MSG_EXPIRY) { export const hasRateLimit = process.env.RATE_LIMITER === 'true'; export const redisCluster = process.env.REDIS_CLUSTER === 'true'; export const redisTLS = process.env.REDIS_TLS === 'true'; + +export const analyticsServerUrl = + process.env.ANALYTICS_SERVER_URL || 'http://localhost:2002'; diff --git a/packages/sdk-socket-server-next/src/index.ts b/packages/sdk-socket-server-next/src/index.ts index 7171ca2f2..6cb3ff237 100644 --- a/packages/sdk-socket-server-next/src/index.ts +++ b/packages/sdk-socket-server-next/src/index.ts @@ -7,11 +7,9 @@ dotenv.config(); // Load config import { instrument } from '@socket.io/admin-ui'; -import packageJson from '../package.json'; import { isDevelopment, withAdminUI } from './config'; -import { analytics, app } from './analytics-api'; +import { app } from './app'; import { getLogger } from './logger'; -import { readMetrics } from './metrics'; import { configureSocketServer } from './socket-config'; import { cleanupAndExit } from './utils'; @@ -20,11 +18,11 @@ const logger = getLogger(); // Register event listeners for process termination events process.on('SIGINT', async () => { - await cleanupAndExit(server, analytics); + await cleanupAndExit(server); }); process.on('SIGTERM', async () => { - await cleanupAndExit(server, analytics); + await cleanupAndExit(server); }); process.on('unhandledRejection', (reason, promise) => { @@ -34,7 +32,7 @@ process.on('unhandledRejection', (reason, promise) => { configureSocketServer(server) .then((ioServer) => { logger.info( - `socker.io server started development=${isDevelopment} adminUI=${withAdminUI}`, + `socket.io server started development=${isDevelopment} adminUI=${withAdminUI}`, ); if (withAdminUI) { @@ -46,16 +44,6 @@ configureSocketServer(server) }); } - // Make sure to protect the endpoint to be only available within the cluster for prometheus - app.get('/metrics', async (_req, res) => { - res.set('Content-Type', 'text/plain'); - res.send(await readMetrics()); - }); - - app.get('/version', (_req, res) => { - res.send({ version: packageJson.version }); - }); - const port: number = Number(process.env.PORT) || 4000; server.listen(port, () => { logger.info(`listening on *:${port}`); diff --git a/packages/sdk-socket-server-next/src/metrics.ts b/packages/sdk-socket-server-next/src/metrics.ts index 2d686a634..836dfd925 100644 --- a/packages/sdk-socket-server-next/src/metrics.ts +++ b/packages/sdk-socket-server-next/src/metrics.ts @@ -6,6 +6,7 @@ import { Registry, Summary, } from 'prom-client'; +import { getLogger } from './logger'; const register = new Registry(); @@ -271,10 +272,10 @@ export function incrementAnalyticsEvents( sdkVersion: string, ) { analyticsEventsTotal.inc({ - from: from, + from, with_channel_id: withChannelId ? 'true' : 'false', event_name: eventName, - platform: platform, + platform, sdk_version: sdkVersion, }); } @@ -385,3 +386,31 @@ export function observeLeaveChannelDuration(duration: number) { export function observeCheckRoomDuration(duration: number) { checkRoomDuration.observe(duration); } + +// Add a counter for overall migration progress +let totalKeysMigrated = 0; + +// Add migration metrics to track conversion from old to new Redis key formats +export const incrementKeyMigration = ({ + migrationType, +}: { + migrationType: string; +}) => { + totalKeysMigrated += 1; + incrementRedisCacheOperation(`migration-${migrationType}`, true); + + // Log migration progress when reaching certain thresholds + if (totalKeysMigrated % 100 === 0) { + getLogger().info( + `Migration progress: ${totalKeysMigrated} total keys migrated so far`, + ); + } +}; + +// Add a function to get migration stats for monitoring +export const getMigrationStats = () => { + return { + totalKeysMigrated, + timestamp: new Date().toISOString(), + }; +}; diff --git a/packages/sdk-socket-server-next/src/middleware-analytics-redirect.ts b/packages/sdk-socket-server-next/src/middleware-analytics-redirect.ts new file mode 100644 index 000000000..6c954f5a3 --- /dev/null +++ b/packages/sdk-socket-server-next/src/middleware-analytics-redirect.ts @@ -0,0 +1,22 @@ +import { + Request as ExpressRequest, + Response as ExpressResponse, + NextFunction, +} from 'express'; +import { analyticsServerUrl } from './config'; +import { getLogger } from './logger'; + +const logger = getLogger(); + +export const analyticsRedirectMiddleware = ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, +): void => { + if (req.path === '/evt' || req.path === '/debug') { + const targetUrl = `${analyticsServerUrl}${req.path}`; + logger.debug(`Redirecting analytics request to ${targetUrl}`); + return res.redirect(307, targetUrl); + } + return next(); +}; diff --git a/packages/sdk-socket-server-next/src/middleware-metrics.ts b/packages/sdk-socket-server-next/src/middleware-metrics.ts index 4f3958438..15a84f945 100644 --- a/packages/sdk-socket-server-next/src/middleware-metrics.ts +++ b/packages/sdk-socket-server-next/src/middleware-metrics.ts @@ -1,21 +1,25 @@ -import { NextFunction, Request, Response } from 'express'; import { - setAnalyticsRequestDuration, - setAnalyticsRequestsTotal, + NextFunction, + Request as ExpressRequest, + Response as ExpressResponse, +} from 'express'; +import { + setAnalyticsRequestDuration, + setAnalyticsRequestsTotal, } from './metrics'; export function evtMetricsMiddleware( - req: Request, - res: Response, - next: NextFunction, + _req: ExpressRequest, + res: ExpressResponse, + next: NextFunction, ): void { - const startTime = Date.now(); + const startTime = Date.now(); - res.on('finish', () => { - const duration = (Date.now() - startTime) / 1000; - setAnalyticsRequestsTotal(res.statusCode); - setAnalyticsRequestDuration(duration); - }); + res.on('finish', () => { + const duration = (Date.now() - startTime) / 1000; + setAnalyticsRequestsTotal(res.statusCode); + setAnalyticsRequestDuration(duration); + }); - next(); + next(); } diff --git a/packages/sdk-socket-server-next/src/protocol/handleAck.ts b/packages/sdk-socket-server-next/src/protocol/handleAck.ts index 54215f860..9622b38bb 100644 --- a/packages/sdk-socket-server-next/src/protocol/handleAck.ts +++ b/packages/sdk-socket-server-next/src/protocol/handleAck.ts @@ -1,7 +1,8 @@ import { Server, Socket } from 'socket.io'; -import { pubClient } from '../analytics-api'; +import { pubClient } from '../redis'; import { getLogger } from '../logger'; -import { ClientType } from '../socket-config'; +import { ClientType } from '../socket-types'; +import { incrementKeyMigration } from '../metrics'; import { QueuedMessage } from './handleMessage'; const logger = getLogger(); @@ -23,16 +24,47 @@ export const handleAck = async ({ socket, clientType, }: ACKParams): Promise => { - // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) + // Force keys into the same hash slot in Redis Cluster, using a hash tag const queueKey = `queue:{${channelId}}:${clientType}`; + // Legacy key without hash tag for backward compatibility + const legacyQueueKey = `queue:${channelId}:${clientType}`; + let messages: string[] = []; const socketId = socket.id; const clientIp = socket.request.socket.remoteAddress; + try { - // Retrieve all messages to find and remove the specified one - const rawMessages = await pubClient.lrange(queueKey, 0, -1); - messages = rawMessages.map((item) => + // Try new format first using pubClient wrapper + let rawMessages = await pubClient.lrange(queueKey, 0, -1); + + // If no messages found with new format, try legacy format and migrate if needed + if (rawMessages.length === 0) { + const legacyRawMessages = await pubClient.lrange(legacyQueueKey, 0, -1); + + if (legacyRawMessages.length > 0) { + incrementKeyMigration({ migrationType: 'ack-queue' }); + logger.info( + `Migrating ${legacyRawMessages.length} messages from ${legacyQueueKey} to ${queueKey}`, + ); + + // Use pipeline for efficiency - note: pipeline uses global Redis client in wrapper + const pipeline = pubClient.pipeline(); + + // Add all messages to the new queue + for (const msg of legacyRawMessages) { + pipeline.rpush(queueKey, msg); + } + + // Set expiry on the new queue + pipeline.expire(queueKey, 3600); // 1 hour expiry + + // Process from legacy messages in this run + rawMessages = legacyRawMessages; + } + } + + messages = rawMessages.map((item: string) => Array.isArray(item) ? item[1] : item, ); diff --git a/packages/sdk-socket-server-next/src/protocol/handleChannelRejected.ts b/packages/sdk-socket-server-next/src/protocol/handleChannelRejected.ts index 3faff9554..9a5dff8e2 100644 --- a/packages/sdk-socket-server-next/src/protocol/handleChannelRejected.ts +++ b/packages/sdk-socket-server-next/src/protocol/handleChannelRejected.ts @@ -1,5 +1,5 @@ import { Server, Socket } from 'socket.io'; -import { pubClient, pubClientPool } from '../analytics-api'; +import { pubClient } from '../redis'; import { config } from '../config'; import { getLogger } from '../logger'; import { ChannelConfig } from './handleJoinChannel'; @@ -29,69 +29,84 @@ export const handleChannelRejected = async ( // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) const channelConfigKey = `channel_config:{${channelId}}`; - const existingConfig = await pubClient.get(channelConfigKey); - let channelConfig: ChannelConfig | null = existingConfig - ? (JSON.parse(existingConfig) as ChannelConfig) - : null; - if (channelConfig) { - logger.debug( - `[handleChannelRejected] Channel already exists: ${channelId}`, - JSON.stringify(channelConfig), - ); + try { + // Get existing config using pubClient wrapper + const existingConfig = await pubClient.get(channelConfigKey); + let channelConfig: ChannelConfig | null = existingConfig + ? (JSON.parse(existingConfig) as ChannelConfig) + : null; - // ignore if already ready - if (channelConfig.ready) { - logger.warn( - `[handleChannelRejected] received rejected for channel that is already ready: ${channelId}`, - { - channelId, - socketId, - clientIp, - }, + if (channelConfig) { + logger.debug( + `[handleChannelRejected] Channel already exists: ${channelId}`, + JSON.stringify(channelConfig), ); - return; - } - // channel config already exists but keyexchange hasn't happened, so we can just update the existing one as rejected with short ttl. - channelConfig.rejected = true; - channelConfig.updatedAt = Date.now(); - } else { - // this condition can occur if the dapp (ios) was disconnected before the channel config was created - channelConfig = { - clients: { - wallet: socketId, - dapp: '', - }, - rejected: true, - createdAt: Date.now(), - updatedAt: Date.now(), - }; - } + // ignore if already ready + if (channelConfig.ready) { + logger.warn( + `[handleChannelRejected] received rejected for channel that is already ready: ${channelId}`, + { + channelId, + socketId, + clientIp, + }, + ); + callback?.(null, { success: false, reason: 'channel_already_ready' }); + return; + } - logger.info( - `[handleChannelRejected] updating channel config for channelId=${channelId}`, - { - channelId, - socketId, - clientIp, - }, - ); + // channel config already exists but keyexchange hasn't happened, so we can just update the existing one as rejected with short ttl. + channelConfig.rejected = true; + channelConfig.updatedAt = Date.now(); + } else { + // this condition can occur if the dapp (ios) was disconnected before the channel config was created + channelConfig = { + clients: { + wallet: socketId, + dapp: '', + }, + rejected: true, + createdAt: Date.now(), + updatedAt: Date.now(), + }; + } - const client = await pubClientPool.acquire(); + logger.info( + `[handleChannelRejected] updating channel config for channelId=${channelId}`, + { + channelId, + socketId, + clientIp, + }, + ); - // Update redis channel config to inform dApp of rejection - await client.setex( - channelConfigKey, - config.rejectedChannelExpiry, - JSON.stringify(channelConfig), - ); + // Update redis channel config to inform dApp of rejection using pubClient wrapper + await pubClient.setex( + channelConfigKey, + config.rejectedChannelExpiry, + JSON.stringify(channelConfig), + ); - await pubClientPool.release(client); + // Also broadcast to dapp if it is connected + socket.broadcast.to(channelId).emit(`rejected-${channelId}`, { channelId }); - // Also broadcast to dapp if it is connected - socket.broadcast.to(channelId).emit(`rejected-${channelId}`, { channelId }); + // Edit redis channel config to set to terminated for sdk to pick up + callback?.(null, { success: true }); + } catch (error) { + logger.error( + `[handleChannelRejected] Error for channelId=${channelId}: ${error}`, + { + channelId, + socketId, + clientIp, + }, + ); - // Edit redis channel config to set to terminated for sdk to pick up - callback?.(null, { success: true }); + callback?.( + error instanceof Error ? error.message : 'Unknown error occurred', + undefined, + ); + } }; diff --git a/packages/sdk-socket-server-next/src/protocol/handleCheckRoom.ts b/packages/sdk-socket-server-next/src/protocol/handleCheckRoom.ts index 7c8983bd2..b8c89012a 100644 --- a/packages/sdk-socket-server-next/src/protocol/handleCheckRoom.ts +++ b/packages/sdk-socket-server-next/src/protocol/handleCheckRoom.ts @@ -1,7 +1,7 @@ import { validate } from 'uuid'; import { Server, Socket } from 'socket.io'; import { getLogger } from '../logger'; -import { pubClient } from '../analytics-api'; +import { pubClient } from '../redis'; const logger = getLogger(); @@ -35,15 +35,30 @@ export const handleCheckRoom = async ({ const room = io.sockets.adapter.rooms.get(channelId); const occupancy = room ? room.size : 0; - // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) - const channelOccupancyKey = `channel_occupancy:{${channelId}}`; - const channelOccupancy = - (await pubClient.get(channelOccupancyKey)) ?? undefined; - - logger.info( - `[check_room] occupancy=${occupancy}, channelOccupancy=${channelOccupancy}`, - { socketId, clientIp, channelId }, - ); - // Callback with null as the first argument, meaning "no error" - return callback(null, { occupancy, channelOccupancy }); + + try { + // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) + const channelOccupancyKey = `channel_occupancy:{${channelId}}`; + // Using pubClient wrapper to access redis + const channelOccupancy = + (await pubClient.get(channelOccupancyKey)) ?? undefined; + + logger.info( + `[check_room] occupancy=${occupancy}, channelOccupancy=${channelOccupancy}`, + { socketId, clientIp, channelId }, + ); + // Callback with null as the first argument, meaning "no error" + return callback(null, { occupancy, channelOccupancy }); + } catch (error) { + logger.error(`[check_room] Error for channelId=${channelId}: ${error}`, { + channelId, + socketId, + clientIp, + }); + + return callback( + error instanceof Error ? error : new Error('Unknown error occurred'), + undefined, + ); + } }; diff --git a/packages/sdk-socket-server-next/src/protocol/handleJoinChannel.ts b/packages/sdk-socket-server-next/src/protocol/handleJoinChannel.ts index d501886a3..7a65d6cee 100644 --- a/packages/sdk-socket-server-next/src/protocol/handleJoinChannel.ts +++ b/packages/sdk-socket-server-next/src/protocol/handleJoinChannel.ts @@ -1,11 +1,13 @@ // protocol/handleJoinChannel.ts import { Server, Socket } from 'socket.io'; import { validate } from 'uuid'; -import { pubClient, pubClientPool } from '../analytics-api'; import { MAX_CLIENTS_PER_ROOM, config, isDevelopment } from '../config'; import { getLogger } from '../logger'; +import { incrementKeyMigration } from '../metrics'; import { rateLimiter } from '../rate-limiter'; -import { ClientType, MISSING_CONTEXT } from '../socket-config'; +import { pubClient } from '../redis'; +import { MISSING_CONTEXT } from '../socket-config'; +import { ClientType } from '../socket-types'; import { retrieveMessages } from './retrieveMessages'; const logger = getLogger(); @@ -79,6 +81,7 @@ export const handleJoinChannel = async ({ }: JoinChannelParams) => { const socketId = socket.id; const clientIp = socket.request.socket.remoteAddress; + try { let from = context ?? MISSING_CONTEXT; if (context?.indexOf('metamask-mobile') !== -1) { @@ -117,11 +120,35 @@ export const handleJoinChannel = async ({ let channelConfig: ChannelConfig | null = null; // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) const channelOccupancyKey = `channel_occupancy:{${channelId}}`; + const legacyChannelOccupancyKey = `channel_occupancy:${channelId}`; if (clientType) { // New protocol when clientType is available const channelConfigKey = `channel_config:{${channelId}}`; - const existingConfig = await pubClient.get(channelConfigKey); + const legacyChannelConfigKey = `channel_config:${channelId}`; + + // Try new key format first using pubClient wrapper + let existingConfig = await pubClient.get(channelConfigKey); + + // If not found with new key, try legacy key + if (!existingConfig) { + existingConfig = await pubClient.get(legacyChannelConfigKey); + + // If found with legacy key, migrate to new key format + if (existingConfig) { + await pubClient.set( + channelConfigKey, + existingConfig, + 'EX', + config.channelExpiry, + ); + incrementKeyMigration({ migrationType: 'channel-config-join' }); + logger.info( + `Migrated channel config from ${legacyChannelConfigKey} to ${channelConfigKey}`, + ); + } + } + channelConfig = existingConfig ? JSON.parse(existingConfig) : null; const now = Date.now(); @@ -182,19 +209,37 @@ export const handleJoinChannel = async ({ JSON.stringify(channelConfig), ); - const client = await pubClientPool.acquire(); - - await client.setex( + // Always write to new key format using pubClient wrapper + await pubClient.setex( channelConfigKey, config.channelExpiry, JSON.stringify(channelConfig), ); // 1 week expiration + } + } + + // Try new key format first using pubClient wrapper + let sRedisChannelOccupancy = await pubClient.get(channelOccupancyKey); + + // If not found with new key, try legacy key + if (!sRedisChannelOccupancy) { + sRedisChannelOccupancy = await pubClient.get(legacyChannelOccupancyKey); - await pubClientPool.release(client); + // If found with legacy key, migrate to new key format + if (sRedisChannelOccupancy) { + await pubClient.set( + channelOccupancyKey, + sRedisChannelOccupancy, + 'EX', + config.channelExpiry, + ); + incrementKeyMigration({ migrationType: 'channel-occupancy' }); + logger.info( + `Migrated channel occupancy from ${legacyChannelOccupancyKey} to ${channelOccupancyKey}`, + ); } } - const sRedisChannelOccupancy = await pubClient.get(channelOccupancyKey); let channelOccupancy = 0; logger.debug( @@ -208,7 +253,8 @@ export const handleJoinChannel = async ({ `[handleJoinChannel] ${channelId} from ${socketId} -- room not found -- creating it now`, ); - await pubClient.set(channelOccupancyKey, 0); + // Set with expiry to ensure the key doesn't live indefinitely if join fails later + await pubClient.setex(channelOccupancyKey, config.channelExpiry, '0'); } // room should be < MAX_CLIENTS_PER_ROOM since we haven't joined yet @@ -347,5 +393,10 @@ export const handleJoinChannel = async ({ socketId, clientIp, }); + + callback?.( + error instanceof Error ? error.message : 'Unknown error occurred', + undefined, + ); } }; diff --git a/packages/sdk-socket-server-next/src/protocol/handleMessage.ts b/packages/sdk-socket-server-next/src/protocol/handleMessage.ts index 5ed0a6668..0668b8694 100644 --- a/packages/sdk-socket-server-next/src/protocol/handleMessage.ts +++ b/packages/sdk-socket-server-next/src/protocol/handleMessage.ts @@ -1,19 +1,59 @@ import { Server, Socket } from 'socket.io'; import { v4 as uuidv4 } from 'uuid'; -import { pubClient } from '../analytics-api'; import { config, isDevelopment } from '../config'; import { getLogger } from '../logger'; +import { incrementKeyMigration } from '../metrics'; import { increaseRateLimits, rateLimiterMessage, resetRateLimits, setLastConnectionErrorTimestamp, } from '../rate-limiter'; -import { ClientType, MISSING_CONTEXT } from '../socket-config'; +import { pubClient } from '../redis'; +import { MISSING_CONTEXT } from '../socket-config'; +import { ClientType } from '../socket-types'; import { ChannelConfig } from './handleJoinChannel'; const logger = getLogger(); +// Add backward compatibility helpers +const getChannelConfigWithBackwardCompatibility = async ({ + channelId, +}: { + channelId: string; +}) => { + try { + // Try new key format first using pubClient wrapper + const channelConfigKey = `channel_config:{${channelId}}`; + const legacyChannelConfigKey = `channel_config:${channelId}`; + let existingConfig = await pubClient.get(channelConfigKey); + + // If not found, try legacy key + if (!existingConfig) { + existingConfig = await pubClient.get(legacyChannelConfigKey); + + // If found with legacy key, migrate to new format + if (existingConfig) { + await pubClient.set( + channelConfigKey, + existingConfig, + 'EX', + config.channelExpiry, + ); + incrementKeyMigration({ migrationType: 'channel-config' }); + logger.info( + `Migrated channel config from ${legacyChannelConfigKey} to ${channelConfigKey}`, + ); + } + } + + return existingConfig ? JSON.parse(existingConfig) : null; + } catch (error) { + logger.error(`[getChannelConfigWithBackwardCompatibility] Error: ${error}`); + return null; + } +}; + export type MessageParams = { io: Server; socket: Socket; @@ -60,10 +100,9 @@ export const handleMessage = async ({ try { if (clientType) { // new protocol, get channelConfig - // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) - const channelConfigKey = `channel_config:{${channelId}}`; - const existingConfig = await pubClient.get(channelConfigKey); - channelConfig = existingConfig ? JSON.parse(existingConfig) : null; + channelConfig = await getChannelConfigWithBackwardCompatibility({ + channelId, + }); ready = channelConfig?.ready ?? false; } @@ -89,8 +128,10 @@ export const handleMessage = async ({ ready = true; channelConfig = { ...channelConfig, ready }; - await pubClient.set( + // Update channel config with pubClient wrapper + await pubClient.setex( `channel_config:{${channelId}}`, + config.channelExpiry, // Refresh expiry when setting ready flag JSON.stringify(channelConfig), ); @@ -114,7 +155,7 @@ export const handleMessage = async ({ ackId = uuidv4(); // Store in the correct message queue const otherQueue = clientType === 'dapp' ? 'wallet' : 'dapp'; - // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) + // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) const queueKey = `queue:{${channelId}}:${otherQueue}`; const persistedMsg: QueuedMessage = { message, @@ -126,6 +167,8 @@ export const handleMessage = async ({ `[handleMessage] persisting message in queue ${queueKey}`, persistedMsg, ); + + // Use pubClient wrapper for persistence await pubClient.rpush(queueKey, JSON.stringify(persistedMsg)); await pubClient.expire(queueKey, config.msgExpiry); } diff --git a/packages/sdk-socket-server-next/src/protocol/handlePing.ts b/packages/sdk-socket-server-next/src/protocol/handlePing.ts index 4994708c2..f20d44521 100644 --- a/packages/sdk-socket-server-next/src/protocol/handlePing.ts +++ b/packages/sdk-socket-server-next/src/protocol/handlePing.ts @@ -2,7 +2,7 @@ import { Server, Socket } from 'socket.io'; import { validate } from 'uuid'; import { isDevelopment } from '../config'; import { getLogger } from '../logger'; -import { ClientType } from '../socket-config'; +import { ClientType } from '../socket-types'; import { retrieveMessages } from './retrieveMessages'; const logger = getLogger(); @@ -43,7 +43,7 @@ export const handlePing = async ({ ); if (clientType) { - // Check for pending messages + // Check for pending messages - retrieveMessages now uses the connection pool internally const messages = await retrieveMessages({ channelId, clientType }); if (messages.length > 0) { logger.debug( diff --git a/packages/sdk-socket-server-next/src/protocol/retrieveMessages.ts b/packages/sdk-socket-server-next/src/protocol/retrieveMessages.ts index b8e57792b..16ca41235 100644 --- a/packages/sdk-socket-server-next/src/protocol/retrieveMessages.ts +++ b/packages/sdk-socket-server-next/src/protocol/retrieveMessages.ts @@ -1,6 +1,7 @@ -import { pubClient } from '../analytics-api'; +import { pubClient } from '../redis'; import { getLogger } from '../logger'; -import { ClientType } from '../socket-config'; +import { ClientType } from '../socket-types'; +import { incrementKeyMigration } from '../metrics'; import { QueuedMessage } from './handleMessage'; const logger = getLogger(); @@ -12,10 +13,47 @@ export const retrieveMessages = async ({ channelId: string; clientType: ClientType; }): Promise => { - // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) + // Force keys into the same hash slot in Redis Cluster, using a hash tag const queueKey = `queue:{${channelId}}:${clientType}`; + // Legacy key without hash tag for backward compatibility + const legacyQueueKey = `queue:${channelId}:${clientType}`; + try { - const messageData = await pubClient.lrange(queueKey, 0, -1); + // Try new format first using pubClient wrapper + let messageData = await pubClient.lrange(queueKey, 0, -1); + + // If no messages found with new format, try legacy format + if (messageData.length === 0) { + const legacyMessageData = await pubClient.lrange(legacyQueueKey, 0, -1); + + // If found messages in legacy format, migrate them to new format + if (legacyMessageData.length > 0) { + incrementKeyMigration({ migrationType: 'message-queue' }); + logger.info( + `Migrating ${legacyMessageData.length} messages from ${legacyQueueKey} to ${queueKey}`, + ); + + // Use pipeline for efficiency - note: pipeline uses global client in wrapper + const pipeline = pubClient.pipeline(); + + // Add all messages to the new queue + for (const msg of legacyMessageData) { + pipeline.rpush(queueKey, msg); + } + + // Set expiry on the new queue + pipeline.expire(queueKey, 3600); // 1 hour expiry + + // Delete the old queue after migration + pipeline.del(legacyQueueKey); + + await pipeline.exec(); + + // Use the legacy data for this request + messageData = legacyMessageData; + } + } + const messages = messageData .map((msg) => JSON.parse(msg) as QueuedMessage) .filter((msg) => msg.message); diff --git a/packages/sdk-socket-server-next/src/redis-check.ts b/packages/sdk-socket-server-next/src/redis-check.ts index 9123f74ae..a518cb8b7 100644 --- a/packages/sdk-socket-server-next/src/redis-check.ts +++ b/packages/sdk-socket-server-next/src/redis-check.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; // Dotenv must be loaded before importing local files dotenv.config(); -import { getGlobalRedisClient } from './analytics-api'; +import { pubClient } from './redis'; import { createLogger } from './logger'; @@ -33,26 +33,25 @@ if (redisNodes.length === 0) { async function testRedisOperations() { try { - // Connect to Redis - const cluster = getGlobalRedisClient(); - logger.info('Connected to Redis Cluster successfully'); - - // Set a key in Redis + // Test Redis connectivity via pubClient wrapper const key = 'testKey'; const value = 'Hello, Redis!'; - logger.info(`Setting ${key} in Redis`); - await cluster.set(key, value, 'EX', 60); // Set key to expire in 60 seconds - logger.info(`Set ${key} in Redis`); - - // Get the key from Redis - const fetchedValue = await cluster.get(key); - logger.info(`Got value from Redis: ${fetchedValue}`); - // Disconnect from Redis - cluster.disconnect(); - logger.info('Disconnected from Redis Cluster'); + // Set, get, delete as a single operation test + logger.info('Testing Redis operations...'); + await pubClient.set(key, value, 'EX', '60'); + const fetchedValue = await pubClient.get(key); + await pubClient.del(key); + + if (fetchedValue === value) { + logger.info('✅ Redis operations completed successfully'); + } else { + logger.error( + `❌ Redis value mismatch: expected '${value}', got '${fetchedValue}'`, + ); + } } catch (error) { - logger.error('Redis operation failed:', error); + logger.error('❌ Redis operation failed:', error); } } diff --git a/packages/sdk-socket-server-next/src/redis.ts b/packages/sdk-socket-server-next/src/redis.ts new file mode 100644 index 000000000..f0cdf51cf --- /dev/null +++ b/packages/sdk-socket-server-next/src/redis.ts @@ -0,0 +1,492 @@ +/* eslint-disable node/no-process-env */ +import { Cluster, ClusterOptions, Redis, RedisOptions } from 'ioredis'; +import genericPool from 'generic-pool'; +import { redisCluster, redisTLS } from './config'; +import { getLogger } from './logger'; +import { incrementRedisCacheOperation } from './metrics'; // Keep metrics import if used by Redis logic + +const logger = getLogger(); + +// Initialize Redis Cluster client +let redisNodes: { + host: string; + port: number; +}[] = []; + +if (process.env.REDIS_NODES) { + // format: REDIS_NODES=redis://rediscluster-redis-cluster-0.rediscluster-redis-cluster-headless.redis.svc.cluster.local:6379,redis://rediscluster-redis-cluster-1.rediscluster-redis-cluster-headless.redis.svc.cluster.local:6379,redis://rediscluster-redis-cluster-2.rediscluster-redis-cluster-headless.redis.svc.cluster.local:6379 + redisNodes = process.env.REDIS_NODES.split(',').map((node) => { + const [host, port] = node.replace('redis://', '').split(':'); + return { + host, + port: parseInt(port, 10), + }; + }); +} +logger.info('Redis nodes:', redisNodes); + +if (redisNodes.length === 0 && process.env.NODE_ENV !== 'test') { + // Allow test env without redis + logger.error('No Redis nodes found'); + process.exit(1); +} + +export const getRedisOptions = ( + isTls: boolean, + password: string | undefined, +): RedisOptions => { + const tlsOptions = { + tls: { + checkServerIdentity: (/* host, cert*/) => { + return undefined; + }, + }, + }; + + const options: RedisOptions = { + ...(isTls && tlsOptions), + connectTimeout: 60000, + keepAlive: 369, + maxRetriesPerRequest: 4, + retryStrategy: (times) => { + const delay = Math.min(times * 30, 1000); + logger.info(`Redis retry attempt ${times} with delay ${delay}ms`); + return delay; + }, + reconnectOnError: (error) => { + const targetErrors = [ + /MOVED/u, + /READONLY/u, + /ETIMEDOUT/u, + /ECONNRESET/u, + /ECONNREFUSED/u, + /EPIPE/u, + /ENOTFOUND/u, + ]; + + logger.error('Redis reconnect error:', error); + return targetErrors.some((targetError) => + targetError.test(error.message), + ); + }, + }; + if (password) { + options.password = password; + } + return options; +}; + +// Cache created clients to reduce connection churn +const redisClientCache = new Map(); + +export const buildRedisClient = (usePipelining = true) => { + let newRedisClient: Cluster | Redis | undefined; + + // Only log connection attempts at debug level unless first time + const logLevel = redisClientCache.size > 0 ? 'debug' : 'info'; + + if (redisNodes.length === 0) { + logger.warn( + 'Skipping Redis client creation as no nodes are defined (likely test environment)', + ); + return undefined; // Return undefined if no nodes + } + + if (redisCluster) { + logger[logLevel]('Connecting to Redis Cluster...'); + + const redisOptions = getRedisOptions(redisTLS, process.env.REDIS_PASSWORD); + const redisClusterOptions: ClusterOptions = { + dnsLookup: (address, callback) => callback(null, address), + scaleReads: 'slave', + slotsRefreshTimeout: 10000, + showFriendlyErrorStack: true, + slotsRefreshInterval: 5000, + natMap: process.env.REDIS_NAT_MAP + ? JSON.parse(process.env.REDIS_NAT_MAP) + : undefined, + redisOptions: { + ...redisOptions, + // Queues commands when disconnected from Redis, executing them when connection is restored + // This prevents data loss during network issues or cluster topology changes + offlineQueue: true, + // Default is 10000ms (10s). Increasing this allows more time to establish + // connection during network instability while balancing real-time requirements + connectTimeout: 20000, + // Default is no timeout. Setting to 10000ms prevents hanging commands + // while still allowing reasonable time for completion + commandTimeout: 10000, + }, + clusterRetryStrategy: (times) => { + const delay = Math.min(times * 100, 5000); + logger.info( + `Redis Cluster retry attempt ${times} with delay ${delay}ms`, + ); + return delay; + }, + enableAutoPipelining: usePipelining, + }; + + logger.debug( + 'Redis Cluster options:', + JSON.stringify(redisClusterOptions, null, 2), + ); + + newRedisClient = new Cluster(redisNodes, redisClusterOptions); + } else { + logger[logLevel]('Connecting to single Redis node'); + newRedisClient = new Redis(redisNodes[0]); + } + + // Reduce connection event noise - only log significant events + newRedisClient.on('ready', () => { + logger.info('Redis ready'); + }); + + newRedisClient.on('error', (error) => { + logger.error('Redis error:', error); + }); + + // Use connectionId to track individual connections in logs without excessive output + const connectionId = + Date.now().toString(36) + Math.random().toString(36).substr(2, 5); + + // Log only once at initialization instead of separate events + logger.debug( + `Redis connection ${connectionId} initialized - events will be handled silently`, + ); + + // Remove these individual event logs to reduce noise + // These events still happen but we don't log each occurrence + newRedisClient.on('connect', () => { + // Silent connection + }); + + newRedisClient.on('close', () => { + // Silent close + }); + + newRedisClient.on('reconnecting', () => { + // Silent reconnection + }); + + newRedisClient.on('end', () => { + // Silent end + }); + + return newRedisClient; +}; + +const redisFactory = { + create: async () => { + // Create a unique key for this client + const cacheKey = `redis-client-${Date.now()}-${Math.random() + .toString(36) + .substring(2, 15)}`; + + // Only log once per 50 clients to reduce noise (increased from 10) + const shouldLog = + redisClientCache.size % 50 === 0 || redisClientCache.size === 0; + if (shouldLog) { + logger.info( + `Redis pool: Creating client (cache size: ${redisClientCache.size})`, + ); + } + + const client = buildRedisClient(false); + if (!client) { + // Handle case where client couldn't be built (e.g., no nodes) + logger.error('Failed to create Redis client in factory'); + // Depending on desired behavior, you might throw an error or return a mock/null client + // For now, let's throw to make the issue explicit + throw new Error('Failed to build Redis client in pool factory'); + } + redisClientCache.set(cacheKey, client); + + // Add client-specific reference to allow cleanup + (client as any).__cacheKey = cacheKey; + + return client; // Resolve with the client directly + }, + destroy: (client: Cluster | Redis) => { + // Get cache key from client if available + const cacheKey = (client as any).__cacheKey; + if (cacheKey) { + redisClientCache.delete(cacheKey); + } + + // Only log once per 50 clients to reduce noise (increased from 10) + const shouldLog = + redisClientCache.size % 50 === 0 || redisClientCache.size === 0; + if (shouldLog) { + logger.info( + `Redis pool: Destroying client (cache size: ${redisClientCache.size})`, + ); + } + + return Promise.resolve(client.disconnect()); + }, +}; + +let redisClient: Cluster | Redis | undefined; + +export const getGlobalRedisClient = () => { + if (!redisClient) { + redisClient = buildRedisClient(); + } + + // Ensure redisClient is defined before returning + if (!redisClient) { + throw new Error('Global Redis client could not be initialized.'); + } + return redisClient; +}; + +export const pubClientPool = genericPool.createPool(redisFactory, { + max: 35, + min: 15, + acquireTimeoutMillis: 15000, + idleTimeoutMillis: 300000, + evictionRunIntervalMillis: 180000, + numTestsPerEvictionRun: 2, + softIdleTimeoutMillis: 240000, +}); + +/** + * PooledClientWrapper - A Redis client wrapper that uses the connection pool internally + * ... (rest of the class definition as before) ... + */ +class PooledClientWrapper { + async get(key: string): Promise { + const client = await pubClientPool.acquire(); + try { + const value = await client.get(key); + incrementRedisCacheOperation('pooled-get', Boolean(value)); // Example metric integration + return value; + } finally { + await pubClientPool.release(client); + } + } + + async set( + key: string, + value: string, + mode?: string, + duration?: string | number, + ): Promise<'OK'> { + const client = await pubClientPool.acquire(); + try { + let result: 'OK'; + if (mode === 'EX' && duration) { + result = await client.set(key, value, mode, duration); + } else { + result = await client.set(key, value); + } + incrementRedisCacheOperation('pooled-set', true); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async setex(key: string, seconds: number, value: string): Promise<'OK'> { + const client = await pubClientPool.acquire(); + try { + const result = await client.setex(key, seconds, value); + incrementRedisCacheOperation('pooled-setex', true); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async incrby(key: string, increment: number): Promise { + const client = await pubClientPool.acquire(); + try { + const result = await client.incrby(key, increment); + incrementRedisCacheOperation('pooled-incrby', true); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async del(key: string): Promise { + const client = await pubClientPool.acquire(); + try { + const result = await client.del(key); + incrementRedisCacheOperation('pooled-del', result > 0); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async ping(): Promise { + const client = await pubClientPool.acquire(); + try { + return await client.ping(); + } finally { + await pubClientPool.release(client); + } + } + + async lrange(key: string, start: number, stop: number): Promise { + const client = await pubClientPool.acquire(); + try { + const result = await client.lrange(key, start, stop); + incrementRedisCacheOperation('pooled-lrange', result.length > 0); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async lset(key: string, index: number, value: string): Promise<'OK'> { + const client = await pubClientPool.acquire(); + try { + const result = await client.lset(key, index, value); + incrementRedisCacheOperation('pooled-lset', true); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async lrem(key: string, count: number, value: string): Promise { + const client = await pubClientPool.acquire(); + try { + const result = await client.lrem(key, count, value); + incrementRedisCacheOperation('pooled-lrem', result > 0); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async rpush(key: string, ...values: string[]): Promise { + const client = await pubClientPool.acquire(); + try { + const result = await client.rpush(key, ...values); + incrementRedisCacheOperation('pooled-rpush', true); + return result; + } finally { + await pubClientPool.release(client); + } + } + + async expire(key: string, seconds: number): Promise { + const client = await pubClientPool.acquire(); + try { + const result = await client.expire(key, seconds); + incrementRedisCacheOperation('pooled-expire', result > 0); + return result; + } finally { + await pubClientPool.release(client); + } + } + + duplicate(): any { + // For socket.io's createAdapter which uses pubClient.duplicate() + // We MUST return the actual Redis client here, not a wrapper + // Socket.io requires the Redis client to be an EventEmitter with .on() methods + // which our wrapper doesn't implement + return getGlobalRedisClient().duplicate(); + } + + disconnect(): void { + // This is a no-op for the wrapper + // The actual client disconnects are managed by the pool + } + + pipeline(): any { + // This is a temporary solution - ideally pipeline operations should be adapted to use the pool + // But for backward compatibility, we'll just use the global client for now + // NOTE: This is not ideal for high concurrency as it bypasses the pool + // TODO: Future improvement would be to acquire a client, run pipeline, then release + const client = getGlobalRedisClient(); + return client.pipeline(); + } +} + +// Export the wrapper as pubClient +export const pubClient = new PooledClientWrapper(); + +// Add Redis health checking and recovery +let redisHealthCheckInterval: NodeJS.Timeout | undefined; +let consecutiveRedisErrors = 0; +const MAX_CONSECUTIVE_ERRORS = 10; + +export async function inspectRedis(key?: string) { + const REDIS_DEBUG_LOGS = process.env.REDIS_DEBUG_LOGS === 'true'; + if (REDIS_DEBUG_LOGS && key && typeof key === 'string') { + // pubClient is a wrapper around the pool, so this is safe + const value = await pubClient.get(key); + logger.debug(`inspectRedis Key: ${key}, Value: ${value}`); + } +} + +// Update the monitorRedisHealth function to use the wrapper +export const monitorRedisHealth = () => { + if (redisHealthCheckInterval) { + clearInterval(redisHealthCheckInterval); + redisHealthCheckInterval = undefined; // Clear the interval ID + } + + // Track health status to only log changes + let isHealthy = false; // Initialize to false to ensure the first success logs + + redisHealthCheckInterval = setInterval(async () => { + try { + // Direct ping with no custom timeout - keep it simple + await pubClient.ping(); + + // Health check succeeded + if (!isHealthy) { + // Transitioning from unhealthy to healthy + const logMessage = + consecutiveRedisErrors > 0 + ? `Redis health restored after ${consecutiveRedisErrors} consecutive errors` // Recovered from errors + : 'Redis health check passed.'; // Initial success + logger.info(logMessage); + + consecutiveRedisErrors = 0; + isHealthy = true; + } + // If isHealthy was already true, do nothing (steady healthy state) + } catch (error) { + consecutiveRedisErrors += 1; + + // Only log the first error transition or milestone errors + if (isHealthy || consecutiveRedisErrors % 5 === 0) { + // Log first time it fails (isHealthy was true) or every 5th failure + logger.error( + `Redis health check failed (${consecutiveRedisErrors}/${MAX_CONSECUTIVE_ERRORS}):`, + error, + ); + isHealthy = false; // Mark as unhealthy now + } + + // If too many consecutive errors, attempt to rebuild the Redis client + if (consecutiveRedisErrors >= MAX_CONSECUTIVE_ERRORS) { + logger.warn( + `Attempting Redis client pool recovery after ${consecutiveRedisErrors} consecutive errors`, + ); + // The pool should handle reconnection automatically based on its strategy. + // We don't need to explicitly rebuild here, just reset the counter maybe? + // For now, just log the attempt and reset counter to prevent spamming logs. + consecutiveRedisErrors = 0; // Reset error count after logging recovery attempt + } + } + }, 30000); // Check every 30 seconds +}; + +// Start monitoring when the module is loaded +// Only start if not in test environment or if redis nodes are configured +if (process.env.NODE_ENV !== 'test' || redisNodes.length > 0) { + monitorRedisHealth(); +} else { + logger.info( + 'Skipping Redis health monitoring in test environment without nodes.', + ); +} diff --git a/packages/sdk-socket-server-next/src/socket-config.ts b/packages/sdk-socket-server-next/src/socket-config.ts index 36b27f520..485a2768a 100644 --- a/packages/sdk-socket-server-next/src/socket-config.ts +++ b/packages/sdk-socket-server-next/src/socket-config.ts @@ -1,25 +1,12 @@ // socket-config.ts -/* eslint-disable node/no-process-env */ import { Server as HTTPServer } from 'http'; import { hostname } from 'os'; import { createAdapter } from '@socket.io/redis-adapter'; import { Server, Socket } from 'socket.io'; import { validate } from 'uuid'; -import { pubClient, pubClientPool } from './analytics-api'; +import { config } from './config'; import { getLogger } from './logger'; -import { ACKParams, handleAck } from './protocol/handleAck'; -import { - ChannelRejectedParams, - handleChannelRejected, -} from './protocol/handleChannelRejected'; -import { handleCheckRoom } from './protocol/handleCheckRoom'; -import { - handleJoinChannel, - JoinChannelParams, -} from './protocol/handleJoinChannel'; -import { handleMessage, MessageParams } from './protocol/handleMessage'; -import { handlePing } from './protocol/handlePing'; import { incrementAck, incrementAckError, @@ -48,13 +35,25 @@ import { setSocketIoServerTotalClients, setSocketIoServerTotalRooms, } from './metrics'; +import { ACKParams, handleAck } from './protocol/handleAck'; +import { + ChannelRejectedParams, + handleChannelRejected, +} from './protocol/handleChannelRejected'; +import { handleCheckRoom } from './protocol/handleCheckRoom'; +import { + handleJoinChannel, + JoinChannelParams, +} from './protocol/handleJoinChannel'; +import { handleMessage, MessageParams } from './protocol/handleMessage'; +import { handlePing } from './protocol/handlePing'; +import { getGlobalRedisClient, pubClient } from './redis'; +import { ClientType } from './socket-types'; const logger = getLogger(); export const MISSING_CONTEXT = '___MISSING_CONTEXT___'; -export type ClientType = 'dapp' | 'wallet'; - export const configureSocketServer = async ( server: HTTPServer, ): Promise => { @@ -66,17 +65,22 @@ export const configureSocketServer = async ( `Start socket server with rate limiter: ${hasRateLimit} - isDevelopment: ${isDevelopment}`, ); - const subClient = pubClient.duplicate(); + const basePubClient = getGlobalRedisClient(); + const baseSubClient = getGlobalRedisClient().duplicate(); - subClient.on('error', (error) => { - logger.error('Redis subClient error:', error); - }); + await new Promise((resolve, reject) => { + baseSubClient.on('ready', () => { + logger.info('Redis subClient ready for adapter'); + resolve(); + }); - subClient.on('ready', () => { - logger.info('Redis subClient ready'); + baseSubClient.on('error', (error: Error) => { + logger.error('Redis subClient error before adapter creation:', error); + reject(error); + }); }); - const adapter = createAdapter(pubClient, subClient); + const adapter = createAdapter(basePubClient.duplicate(), baseSubClient); type SocketJoinChannelParams = { channelId: string; @@ -107,10 +111,18 @@ export const configureSocketServer = async ( // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) const channelOccupancyKey = `channel_occupancy:{${roomId}}`; + // We can use pubClient directly since it's now a wrapper around the pool const channelOccupancy = await pubClient.incrby(channelOccupancyKey, 1); logger.debug( `'join-room' socket ${socketId} has joined room ${roomId} --> channelOccupancy=${channelOccupancy}`, ); + + // If incrby created the key (occupancy is 1), set an initial expiry + if (channelOccupancy === 1) { + // eslint-disable-line no-lonely-if + await pubClient.expire(channelOccupancyKey, config.channelExpiry); + logger.debug(`'join-room' set initial expiry for ${channelOccupancyKey}`); + } }); io.of('/').adapter.on('leave-room', async (roomId, socketId) => { @@ -119,14 +131,13 @@ export const configureSocketServer = async ( // Ignore invalid room IDs return; } - - const client = await pubClientPool.acquire(); // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) const channelOccupancyKey = `channel_occupancy:{${roomId}}`; + // We can use pubClient directly since it's now a wrapper around the pool // Decrement the number of clients in the room - const channelOccupancy = await client.incrby(channelOccupancyKey, -1); + const channelOccupancy = await pubClient.incrby(channelOccupancyKey, -1); logger.debug( `'leave-room' socket ${socketId} has left room ${roomId} --> channelOccupancy=${channelOccupancy}`, @@ -135,10 +146,9 @@ export const configureSocketServer = async ( if (channelOccupancy <= 0) { logger.debug(`'leave-room' room ${roomId} was deleted`); // Force keys into the same hash slot in Redis Cluster, using a hash tag (a substring enclosed in curly braces {}) - const channelOccupancyKey = `channel_occupancy:{${roomId}}`; - // remove from redis - await client.del(channelOccupancyKey); + // remove from redis - use pubClient wrapper that handles pool management internally + await pubClient.del(channelOccupancyKey); } else { logger.info( `'leave-room' Room ${roomId} kept alive with ${channelOccupancy} clients`, @@ -146,8 +156,6 @@ export const configureSocketServer = async ( // Inform the room of the disconnection io.to(roomId).emit(`clients_disconnected-${roomId}`); } - - await pubClientPool.release(client); }); io.on('connection', (socket: Socket) => { @@ -208,13 +216,15 @@ export const configureSocketServer = async ( ) => void; } - handleJoinChannel(params).catch((error) => { - logger.error('Error creating channel:', error); - incrementCreateChannelError(); - }).finally(() => { - const duration = Date.now() - start; - observeCreateChannelDuration(duration); - }); + handleJoinChannel(params) + .catch((error) => { + logger.error('Error creating channel:', error); + incrementCreateChannelError(); + }) + .finally(() => { + const duration = Date.now() - start; + observeCreateChannelDuration(duration); + }); }, ); @@ -239,13 +249,15 @@ export const configureSocketServer = async ( ackId, clientType, }; - handleAck(ackParams).catch((error) => { - logger.error('Error handling ack:', error); - incrementAckError(); - }).finally(() => { - const duration = Date.now() - start; - observeAckDuration(duration); - }); + handleAck(ackParams) + .catch((error) => { + logger.error('Error handling ack:', error); + incrementAckError(); + }) + .finally(() => { + const duration = Date.now() - start; + observeAckDuration(duration); + }); }, ); @@ -284,13 +296,15 @@ export const configureSocketServer = async ( return; } - handleMessage(params).catch((error) => { - logger.error('Error handling message:', error); - incrementMessageError(); - }).finally(() => { - const duration = Date.now() - start; - observeMessageDuration(duration); - }); + handleMessage(params) + .catch((error) => { + logger.error('Error handling message:', error); + incrementMessageError(); + }) + .finally(() => { + const duration = Date.now() - start; + observeMessageDuration(duration); + }); }, ); @@ -315,13 +329,15 @@ export const configureSocketServer = async ( io, clientType, callback, - }).catch((error) => { - logger.error('Error handling ping:', error); - incrementPingError(); - }).finally(() => { - const duration = Date.now() - start; - observePingDuration(duration); - }); + }) + .catch((error) => { + logger.error('Error handling ping:', error); + incrementPingError(); + }) + .finally(() => { + const duration = Date.now() - start; + observePingDuration(duration); + }); }, ); @@ -385,13 +401,15 @@ export const configureSocketServer = async ( const start = Date.now(); incrementJoinChannel(); - handleJoinChannel(params).catch((error) => { - logger.error('Error joining channel:', error); - incrementJoinChannelError(); - }).finally(() => { - const duration = Date.now() - start; - observeJoinChannelDuration(duration); - }); + handleJoinChannel(params) + .catch((error) => { + logger.error('Error joining channel:', error); + incrementJoinChannelError(); + }) + .finally(() => { + const duration = Date.now() - start; + observeJoinChannelDuration(duration); + }); }, ); @@ -404,11 +422,12 @@ export const configureSocketServer = async ( const start = Date.now(); incrementRejected(); - handleChannelRejected({ ...params, io, socket }, callback).catch( - (error) => { + handleChannelRejected({ ...params, io, socket }, callback) + .catch((error) => { logger.error('Error rejecting channel:', error); incrementRejectedError(); - }).finally(() => { + }) + .finally(() => { const duration = Date.now() - start; observeRejectedDuration(duration); }); @@ -449,13 +468,15 @@ export const configureSocketServer = async ( const start = Date.now(); incrementCheckRoom(); - handleCheckRoom({ channelId, io, socket, callback }).catch((error) => { - logger.error('Error checking room:', error); - incrementCheckRoomError(); - }).finally(() => { - const duration = Date.now() - start; - observeCheckRoomDuration(duration); - }); + handleCheckRoom({ channelId, io, socket, callback }) + .catch((error) => { + logger.error('Error checking room:', error); + incrementCheckRoomError(); + }) + .finally(() => { + const duration = Date.now() - start; + observeCheckRoomDuration(duration); + }); }, ); }); diff --git a/packages/sdk-socket-server-next/src/socket-types.ts b/packages/sdk-socket-server-next/src/socket-types.ts new file mode 100644 index 000000000..c055534af --- /dev/null +++ b/packages/sdk-socket-server-next/src/socket-types.ts @@ -0,0 +1 @@ +export type ClientType = 'dapp' | 'wallet'; diff --git a/packages/sdk-socket-server-next/src/utils.ts b/packages/sdk-socket-server-next/src/utils.ts index a2763bcc4..29725c2d6 100644 --- a/packages/sdk-socket-server-next/src/utils.ts +++ b/packages/sdk-socket-server-next/src/utils.ts @@ -1,5 +1,6 @@ import { Server as HttpServer } from 'http'; import { getLogger } from './logger'; +import { getGlobalRedisClient, pubClientPool } from './redis'; const logger = getLogger(); @@ -50,10 +51,7 @@ export const setIsShuttingDown = (value: boolean) => { export const getIsShuttingDown = () => isShuttingDown; -export const cleanupAndExit = async ( - server: Server, - analytics: Analytics, -): Promise => { +export const cleanupAndExit = async (server: Server): Promise => { if (isShuttingDown) { logger.info(`cleanupAndExit already in progress`); return; @@ -61,24 +59,27 @@ export const cleanupAndExit = async ( isShuttingDown = true; try { - const flushAnalyticsResult = await flushAnalytics(analytics); - logger.info(`flushAnalyticsResult: ${flushAnalyticsResult}`); - + logger.info('Starting server cleanup...'); // CloseServer will block until all clients have disconnected. - const serverCloseResult = await closeServer(server); - logger.info(`serverCloseResult: ${serverCloseResult}`); - - if ((serverCloseResult as any) instanceof Error) { - throw new Error(`Error during server shutdown: ${serverCloseResult}`); - } - - if (flushAnalyticsResult instanceof Error) { - throw new Error(`Error on exitGracefully: ${flushAnalyticsResult}`); + await closeServer(server); + logger.info(`HTTP server closed.`); + + logger.info('Draining Redis connection pool...'); + await pubClientPool.drain(); + logger.info('Redis connection pool drained.'); + await pubClientPool.clear(); + logger.info('Redis connection pool cleared.'); + + const globalRedisClient = getGlobalRedisClient(); + if (globalRedisClient && globalRedisClient.status === 'ready') { + logger.info('Disconnecting global Redis client...'); + await globalRedisClient.quit(); + logger.info('Global Redis client disconnected.'); } } catch (error) { - logger.error(`cleanupAndExit error: ${error}`); + logger.error(`Error during cleanup: ${error}`); } finally { - logger.info(`cleanupAndExit done`); + logger.info(`Cleanup finished. Exiting process.`); process.exit(0); } }; diff --git a/packages/sdk-socket-server-next/tsconfig.eslint.json b/packages/sdk-socket-server-next/tsconfig.eslint.json index a336d734e..2f60755b6 100644 --- a/packages/sdk-socket-server-next/tsconfig.eslint.json +++ b/packages/sdk-socket-server-next/tsconfig.eslint.json @@ -10,18 +10,9 @@ "moduleResolution": "Node", "resolveJsonModule": true, "incremental": true, - "lib": [ - "DOM", - "ES2016" - ], + "lib": ["DOM", "ES2016"], "skipLibCheck": true, - "types": [ - "node", - "jest" - ] + "types": ["node", "jest"] }, - "include": [ - "./src/**/*.ts", - "e2e/**/*.ts" - ], + "include": ["./src/**/*.ts", "e2e/**/*.ts"] } diff --git a/packages/sdk-socket-server-next/tsconfig.test.json b/packages/sdk-socket-server-next/tsconfig.test.json index bf0c26068..efc241260 100644 --- a/packages/sdk-socket-server-next/tsconfig.test.json +++ b/packages/sdk-socket-server-next/tsconfig.test.json @@ -9,6 +9,6 @@ "forceConsistentCasingInFileNames": true, "types": ["node", "jest", "@testing-library/jest-dom"] }, - "include": ["src/","e2e/"], + "include": ["src/", "e2e/"], "exclude": ["node_modules"] } diff --git a/packages/sdk/src/services/RemoteConnection/ConnectionInitializer/initializeConnector.ts b/packages/sdk/src/services/RemoteConnection/ConnectionInitializer/initializeConnector.ts index 17e1fe580..fbee6504e 100644 --- a/packages/sdk/src/services/RemoteConnection/ConnectionInitializer/initializeConnector.ts +++ b/packages/sdk/src/services/RemoteConnection/ConnectionInitializer/initializeConnector.ts @@ -31,6 +31,7 @@ export function initializeConnector( dappMetadata: { ...options.dappMetadata, source: options._source }, analytics: options.enableAnalytics, communicationServerUrl: options.communicationServerUrl, + analyticsServerUrl: options.communicationServerUrl, sdkVersion: packageJson.version, context: 'dapp', ecies: options.ecies, diff --git a/yarn.lock b/yarn.lock index 5313f5cc2..f679b9953 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10654,6 +10654,36 @@ __metadata: languageName: node linkType: hard +"@metamask/analytics-server@workspace:packages/analytics-server": + version: 0.0.0-use.local + resolution: "@metamask/analytics-server@workspace:packages/analytics-server" + dependencies: + "@lavamoat/allow-scripts": ^2.3.1 + "@types/analytics-node": ^3.1.13 + "@types/body-parser": ^1.19.4 + "@types/cors": ^2.8.15 + "@types/express": ^4.17.20 + "@types/node": ^20.4.1 + "@typescript-eslint/eslint-plugin": ^4.20.0 + "@typescript-eslint/parser": ^4.20.0 + analytics-node: ^6.2.0 + body-parser: ^1.20.2 + cors: ^2.8.5 + dotenv: ^16.3.1 + eslint: ^7.30.0 + eslint-config-prettier: ^8.3.0 + eslint-plugin-prettier: ^3.4.0 + express: ^4.18.2 + express-rate-limit: ^7.1.5 + helmet: ^5.1.1 + ioredis: ^5.6.0 + prettier: ^2.8.8 + ts-node: ^10.9.1 + typescript: ^4.3.2 + winston: ^3.11.0 + languageName: unknown + linkType: soft + "@metamask/auto-changelog@npm:3.1.0": version: 3.1.0 resolution: "@metamask/auto-changelog@npm:3.1.0" @@ -11167,6 +11197,12 @@ __metadata: languageName: node linkType: hard +"@metamask/sdk-analytics-client@workspace:packages/analytics-client": + version: 0.0.0-use.local + resolution: "@metamask/sdk-analytics-client@workspace:packages/analytics-client" + languageName: unknown + linkType: soft + "@metamask/sdk-communication-layer@npm:0.11.1": version: 0.11.1 resolution: "@metamask/sdk-communication-layer@npm:0.11.1" @@ -11615,7 +11651,7 @@ __metadata: express-rate-limit: ^7.1.5 generic-pool: ^3.9.0 helmet: ^5.1.1 - ioredis: ^5.3.2 + ioredis: ^5.6.0 jest: ^29.6.4 logform: ^2.6.0 lru-cache: ^10.0.0 @@ -11625,7 +11661,7 @@ __metadata: rate-limiter-flexible: ^2.3.8 redis: ^4.6.12 rimraf: ^4.4.0 - socket.io: ^4.4.1 + socket.io: ^4.7.2 socket.io-client: ^4.7.2 supertest: ^6.3.3 ts-jest: ^29.1.1 @@ -26144,6 +26180,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:~0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e + languageName: node + linkType: hard + "cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -28549,6 +28592,23 @@ __metadata: languageName: node linkType: hard +"engine.io@npm:~6.6.0": + version: 6.6.4 + resolution: "engine.io@npm:6.6.4" + dependencies: + "@types/cors": ^2.8.12 + "@types/node": ">=10.0.0" + accepts: ~1.3.4 + base64id: 2.0.0 + cookie: ~0.7.2 + cors: ~2.8.5 + debug: ~4.3.1 + engine.io-parser: ~5.2.1 + ws: ~8.17.1 + checksum: e2d98ed3adc2fe6cdcee7208a95114bc12d3792f69abedcaeaf7cd21aec478f82b84d36f2e59b03af5f6ffae028923c0e799774400c008a768c8ceb17610a7c4 + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.12.0, enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.7.0": version: 5.15.0 resolution: "enhanced-resolve@npm:5.15.0" @@ -34516,6 +34576,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.6.0": + version: 5.6.0 + resolution: "ioredis@npm:5.6.0" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: b085cec251581224c6b9e3e4b0c1f92f99a272976ebcad552bc9d0c63d31abbe0208294b3acedeae4f29759ff3821478727207a47597e2ba081b1036fbc69181 + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -48745,6 +48822,21 @@ __metadata: languageName: node linkType: hard +"socket.io@npm:^4.7.2": + version: 4.8.1 + resolution: "socket.io@npm:4.8.1" + dependencies: + accepts: ~1.3.4 + base64id: ~2.0.0 + cors: ~2.8.5 + debug: ~4.3.2 + engine.io: ~6.6.0 + socket.io-adapter: ~2.5.2 + socket.io-parser: ~4.2.4 + checksum: d5e4d7eabba7a04c0d130a7b34c57050a1b4694e5b9eb9bd0a40dd07c1d635f3d5cacc15442f6135be8b2ecdad55dad08ee576b5c74864508890ff67329722fa + languageName: node + linkType: hard + "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -54088,7 +54180,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.17.1": +"ws@npm:8.17.1, ws@npm:~8.17.1": version: 8.17.1 resolution: "ws@npm:8.17.1" peerDependencies: