Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement/websockets #614

Open
wants to merge 11 commits into
base: enhancement/options-type-validations
Choose a base branch
from
12 changes: 12 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,15 @@ DEBUG_LISTEN_TO_CONSOLE = false
DEBUG_DUMPIO = false
DEBUG_SLOW_MO = 0
DEBUG_DEBUGGING_PORT = 9222

# WEBSOCKET CONFIG
WEB_SOCKET_ENABLE = false
WEB_SOCKET_RECONNECT = false
WEB_SOCKET_REJECT_UNAUTHORIZED = false
WEB_SOCKET_PING_TIMEOUT = 16000
WEB_SOCKET_RECONNECT_INTERVAL = 3000
WEB_SOCKET_RECONNECT_ATTEMPTS = 3
WEB_SOCKET_MESSAGE_INTERVAL = 3600000
WEB_SOCKET_GATHER_ALL_OPTIONS = false
WEB_SOCKET_URL =
WEB_SOCKET_SECRET =
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ _New Features:_
- Added the `validateOption` function for validating a single option. It is used in the code to validate individual options (`svg`, `instr`, `resources`, `customCode`, `callback`, `globalOptions`, and `themeOptions`) loaded from a file.
- Added the `validateOptions` function for validating the full set of options. It is used in the code to validate options coming from functions that update global options, CLI arguments, configurations loaded via `--loadConfig`, and configurations created using the prompts functionality.
- Introduced redefined `getOptions` and `updateOptions` functions to retrieve and update the original global options or a copy of global options, allowing flexibility in export scenarios.
- Introduced the ability to enable a customizable `WebSocket` connection between the Export Server instance and any server or service that supports such connections to collect chart options usage data. This is useful for gathering telemetry data.
- Added a simple filtering mechanism (based on the `./lib/schemas/telemetry.json` file) to control the data being sent.
- Added a new option called `uploadLimit` to control the maximum size of a request's payload body.
- Added the possibility to return a Base64 version of the chart using any export method (not only through requests).
- Added support for displaying CLI usage (`-h`, `--h`, `-help`, `--help`) and version information with license details (`-v`, `--v`).
Expand Down Expand Up @@ -135,6 +137,7 @@ _Enhancements:_
- Fixed an incorrect version change endpoint description in the `Switching Highcharts Version at Runtime` section.
- Corrected example and added description of the `Node.js Module` section.
- Refreshed, expanded, and completely redefined the API documentation.
- Added a `WebSocket` section containing descriptions and information about this feature.
- Added a note on help and version information (`Note About Version and Help Information` section).
- Added a note about path interpretation for properties requiring path settings (`Note About Paths` section).
- Corrected the `Note About Chart Size` section.
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,17 @@ _Available default JSON config:_
"dumpio": false,
"slowMo": 0,
"debuggingPort": 9222
},
"webSocket": {
"enable": false,
"reconnect": false,
"rejectUnauthorized": false,
"pingTimeout": 16000,
"reconnectInterval": 3000,
"reconnectAttempts": 3,
"messageInterval": 3600000,
"gatherAllOptions": false,
"url": null
}
}
```
Expand Down Expand Up @@ -445,6 +456,19 @@ _Available environment variables:_
- `DEBUG_SLOW_MO`: Slows down Puppeteer operations by the specified number of milliseconds (defaults to `0`).
- `DEBUG_DEBUGGING_PORT`: Specifies the debugging port (defaults to `9222`).

### WebSocket Config

- `WEB_SOCKET_ENABLE`: Enables or disables the WebSocket connection (defaults to `false`).
- `WEB_SOCKET_RECONNECT`: Controls whether or not to try reconnecting to the WebSocket server in case of a disconnect (defaults to `false`).
- `WEB_SOCKET_REJECT_UNAUTHORIZED`: Determines whether the client verifies the server's SSL/TLS certificate during the handshake process (defaults to `false`).
- `WEB_SOCKET_PING_TIMEOUT`: The timeout, in milliseconds, for the heartbeat mechanism between the client and server (defaults to `16000`).
- `WEB_SOCKET_RECONNECT_INTERVAL`: The interval, in milliseconds, for the reconnect attempt (defaults to `3000`).
- `WEB_SOCKET_RECONNECT_ATTEMPTS`: The number of reconnect attempts before returning a connection error (defaults to `3`).
- `WEB_SOCKET_MESSAGE_INTERVAL`: The interval, in milliseconds, for auto sending the data through a WebSocket connection (defaults to `3600000`).
- `WEB_SOCKET_GATHER_ALL_OPTIONS`: Decides whether or not to gather all chart's options or only ones defined in the **telemetry.json** file (defaults to `false`).
- `WEB_SOCKET_URL`: The URL of the WebSocket server (defaults to ``).
- `WEB_SOCKET_SECRET`: The secret used to create a JSON Web Token sent to the WebSocket server (defaults to ``).

## Custom JSON Config

To load an additional JSON configuration file, use the `--loadConfig <filepath>` option. This JSON file can either be manually created or generated through a prompt triggered by the `--createConfig <filepath>` option. The `<filepath>` value does not need a _.json_ extension, but the file's content must be valid JSON when using the `--loadConfig` option.
Expand Down Expand Up @@ -581,6 +605,18 @@ _Available CLI arguments:_
- `--slowMo`: Slows down Puppeteer operations by the specified number of milliseconds (defaults to `0`).
- `--debuggingPort`: Specifies the debugging port (defaults to `9222`).

### WebSocket Config

- `--enableWs`: Enables or disables the WebSocket connection (defaults to `false`).
- `--wsReconnect`: Controls whether or not to try reconnecting to the WebSocket server in case of a disconnect (defaults to `false`).
- `--wsRejectUnauthorized`: Determines whether the client verifies the server's SSL/TLS certificate during the handshake process (defaults to `false`).
- `--wsPingTimeout`: The timeout, in milliseconds, for the heartbeat mechanism between the client and server (defaults to `16000`).
- `--wsReconnectInterval`: The interval, in milliseconds, for the reconnect attempt (defaults to `3000`).
- `--wsReconnectAttempts`: The number of reconnect attempts before returning a connection error (defaults to `3`).
- `--wsMessageInterval`: The interval, in milliseconds, for auto sending the data through a WebSocket connection (defaults to `3600000`).
- `--wsGatherAllOptions`: Decides whether or not to gather all chart's options or only ones defined in the **telemetry.json** file (defaults to `false`).
- `--wsUrl`: The URL of the WebSocket server (defaults to `null`).

# HTTP Server

Apart from using as a CLI tool, which allows you to run one command at a time, it is also possible to configure the server to accept POST requests. The simplest way to enable the server is to run the command below:
Expand Down Expand Up @@ -878,6 +914,30 @@ This package supports both CommonJS and ES modules.

Samples and tests for every mentioned export method can be found in the `./samples` and `./tests` folders. Detailed descriptions are available in their corresponding sections on the [Wiki](https://github.com/highcharts/node-export-server/wiki).

# WebSocket

One of the new features that v4 introduces is the ability to configure and establish a WebSocket connection between the Export Server and a user-configured WebSocket server. This can be useful for gathering telemetry data and statistics about the usage of your Export Server instance.

## How It Works

When enabled, a WebSocket connection will be established on Export Server startup. The WebSocket server to which the connection is made must also be configured. The authorization process is completed by sending a JWT generated based on a secret that must also be set on the WebSocket server side.

Once the connection is established, the chart data from each request to the Export Server is passed to the telemetry module, which filters the data based on a JSON file that specifies which data needs to be sent. This file can be found under `./lib/schemas/telemetry.json` and can be modified as needed, but the proposed structure must be maintained. It is also possible to send the entire object of chart options by setting the `gatherAllOptions` option to **true**.

Data processed in this way is saved in an object that collects information about requests for a certain period and is batch sent to the WebSocket server at specified intervals. After sending, the object is cleared of request data and gathers new data until the next interval.

Please refer to the [Configuration](https://github.com/highcharts/node-export-server?tab=readme-ov-file#configuration) section for descriptions of the options.

## Additional Notes

- In order for the heartbeat mechanism between the WebSocket client and server to work correctly, the `pingTimeout` should be set to a higher value in milliseconds than its equivalent in the WebSocket server.

- Setting **0** for any of the interval/timeout-related options (`pingTimeout`, `reconnectInterval`, or `messageInterval`) will disable that option. Bear in mind, however, that disabling `pingTimeout` might result in WebSocket clients not being terminated if no response is received from the server.

- Disabling `pingTimeout` will also disable the reconnect mechanism.

- When using a self-signed certificate (for example, for testing purposes), the `rejectUnauthorized` option should be disabled. Otherwise, it will result in an error and the connection to the WebSocket server will fail.

# Tips, Tricks & Notes

## Note About Version And Help Information
Expand Down
4 changes: 4 additions & 0 deletions lib/resourceRelease.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { killPool } from './pool.js';
import { clearAllTimers } from './timer.js';

import { closeServers } from './server/server.js';
import { terminateClients } from './server/webSocket.js';

/**
* Performs cleanup operations to ensure a graceful shutdown of the process.
Expand All @@ -39,6 +40,9 @@ export async function shutdownCleanUp(exitCode = 0) {
// Clear all ongoing intervals
clearAllTimers(),

// Terminate all connected WebSocket clients
terminateClients(),

// Get available server instances (HTTP/HTTPS) and close them
closeServers(),

Expand Down
97 changes: 97 additions & 0 deletions lib/schemas/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,103 @@ const defaultConfig = {
type: 'number'
}
}
},
webSocket: {
enable: {
value: false,
types: ['boolean'],
envLink: 'WEB_SOCKET_ENABLE',
cliName: 'enableWs',
description: 'Enables or disables the WebSocket connection',
promptOptions: {
type: 'toggle'
}
},
reconnect: {
value: false,
types: ['boolean'],
envLink: 'WEB_SOCKET_RECONNECT',
cliName: 'wsReconnect',
description:
'Whether or not to attempt to reconnect to the WebSocket server if disconnected',
promptOptions: {
type: 'toggle'
}
},
rejectUnauthorized: {
value: false,
types: ['boolean'],
envLink: 'WEB_SOCKET_REJECT_UNAUTHORIZED',
cliName: 'wsRejectUnauthorized',
description:
"Whether or not to client should verify the server's SSL/TLS certificate during the handshake",
promptOptions: {
type: 'toggle'
}
},
pingTimeout: {
value: 16000,
types: ['number'],
envLink: 'WEB_SOCKET_PING_TIMEOUT',
cliName: 'wsPingTimeout',
description:
'Timeout in milliseconds for the heartbeat mechanism between client and server',
promptOptions: {
type: 'number'
}
},
reconnectInterval: {
value: 3000,
types: ['number'],
envLink: 'WEB_SOCKET_RECONNECT_INTERVAL',
cliName: 'wsReconnectInterval',
description: 'Interval in milliseconds between reconnect attempts',
promptOptions: {
type: 'number'
}
},
reconnectAttempts: {
value: 3,
types: ['number'],
envLink: 'WEB_SOCKET_RECONNECT_ATTEMPTS',
cliName: 'wsReconnectAttempts',
description: 'Number of attempts to reconnect before reporting an error',
promptOptions: {
type: 'number'
}
},
messageInterval: {
value: 3600000,
types: ['number'],
envLink: 'WEB_SOCKET_MESSAGE_INTERVAL',
cliName: 'wsMessageInterval',
description:
'Interval in milliseconds for automatically sending data through the WebSocket connection',
promptOptions: {
type: 'number'
}
},
gatherAllOptions: {
value: false,
types: ['boolean'],
envLink: 'WEB_SOCKET_GATHER_ALL_OPTIONS',
cliName: 'wsGatherAllOptions',
description:
'Whether or not to gather all chart options or only those defined in the telemetry.json file',
promptOptions: {
type: 'toggle'
}
},
url: {
value: null,
types: ['string', 'null'],
envLink: 'WEB_SOCKET_URL',
cliName: 'wsUrl',
description: 'URL of the WebSocket server',
promptOptions: {
type: 'text'
}
}
}
};

Expand Down
22 changes: 22 additions & 0 deletions lib/schemas/telemetry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"boost": null,
"chart": {
"type": null,
"options3d": {
"enabled": null
}
},
"colors": null,
"legend": {
"enabled": null
},
"series": {
"type": null
},
"xAxis": {
"type": null
},
"yAxis": {
"type": null
}
}
7 changes: 7 additions & 0 deletions lib/server/routes/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ See LICENSE file in root for details.

import { startExport } from '../../chart.js';
import { log } from '../../logger.js';
import { prepareTelemetry } from '../../telemetry.js';
import { getBase64, measureTime } from '../../utils.js';

import ExportError from '../../errors/ExportError.js';
Expand Down Expand Up @@ -106,6 +107,12 @@ async function requestExport(request, response, next) {
);
}

// Telemetry only for the options based request
if (!options.export.svg) {
// Prepare and send the options through the WebSocket
prepareTelemetry(options.export.options, options.payload.requestId);
}

// Return the result in an appropriate format
if (data.result) {
log(
Expand Down
12 changes: 12 additions & 0 deletions lib/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { updateOptions } from '../config.js';
import { log, logWithStack } from '../logger.js';
import { __dirname, getAbsolutePath } from '../utils.js';

import { webSocketInit } from './webSocket.js';

import errorMiddleware from './middlewares/error.js';
import rateLimitingMiddleware from './middlewares/rateLimiting.js';
import validationMiddleware from './middlewares/validation.js';
Expand Down Expand Up @@ -160,6 +162,11 @@ export async function startServer(serverOptions = {}) {
3,
`[server] Started HTTP server on ${serverOptions.host}:${serverOptions.port}.`
);

if (activeServers.size === 1) {
// Start a WebSocket connection
webSocketInit({ ...httpServer.address(), protocol: 'http' });
}
});
}

Expand Down Expand Up @@ -203,6 +210,11 @@ export async function startServer(serverOptions = {}) {
3,
`[server] Started HTTPS server on ${serverOptions.host}:${serverOptions.ssl.port}.`
);

if (activeServers.size === 1) {
// Start a WebSocket connection
webSocketInit({ ...httpsServer.address(), protocol: 'https' });
}
});
}
}
Expand Down
Loading
Loading