Skip to content

Commit 8dd0620

Browse files
authored
chore: add some basic instrumentation to Node WebSocket server (apache#14354)
* chore: add some basic instrumentation * Switch to statsd using hot-shots * Cleanup a few leftover bits
1 parent 2a1235c commit 8dd0620

File tree

7 files changed

+160
-1
lines changed

7 files changed

+160
-1
lines changed

superset-websocket/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ GLOBAL_ASYNC_QUERIES_JWT_SECRET
9393

9494
More info on Superset configuration values for async queries: https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries
9595

96+
## StatsD monitoring
97+
98+
The application is tracking a couple of metrics with `statsd` using the [hot-shots](https://www.npmjs.com/package/hot-shots) library, such as the number of connected clients and the number of failed attempts to send a message to a client.
99+
100+
`statsd` can be configured with the `statsd` object in the configuration file. See the [hot-shots](https://www.npmjs.com/package/hot-shots) readme for more info. The default configuration is:
101+
102+
```json
103+
{
104+
"statsd": {
105+
"host": "127.0.0.1",
106+
"port": 8125,
107+
"globalTags": []
108+
}
109+
}
110+
```
111+
96112
## Running
97113

98114
Running locally via dev server:

superset-websocket/config.example.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
"logLevel": "info",
44
"logToFile": false,
55
"logFilename": "app.log",
6+
"statsd": {
7+
"host": "127.0.0.1",
8+
"port": 8125,
9+
"globalTags": []
10+
},
611
"redis": {
712
"port": 6379,
813
"host": "127.0.0.1",

superset-websocket/config.test.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
"db": 10,
77
"ssl": false
88
},
9+
"statsd": {
10+
"host": "127.0.0.1",
11+
"port": 8125,
12+
"globalTags": []
13+
},
914
"redisStreamPrefix": "test-async-events-",
1015
"jwtSecret": "test123-test123-test123-test123-test123-test123-test123",
1116
"jwtCookieName": "test-async-token"

superset-websocket/package-lock.json

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

superset-websocket/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"license": "Apache-2.0",
1717
"dependencies": {
1818
"cookie": "^0.4.1",
19+
"hot-shots": "^8.3.1",
1920
"ioredis": "^4.16.1",
2021
"jsonwebtoken": "^8.5.1",
2122
"uuid": "^8.3.2",

superset-websocket/spec/index.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,19 @@ const streamReturnValue: server.StreamResult[] = [
5656
];
5757

5858
import * as server from '../src/index';
59+
import { statsd } from '../src/index';
5960

6061
describe('server', () => {
62+
let statsdIncrementMock: jest.SpyInstance;
63+
6164
beforeEach(() => {
6265
mockRedisXrange.mockClear();
6366
server.resetState();
67+
statsdIncrementMock = jest.spyOn(statsd, 'increment').mockReturnValue();
68+
});
69+
70+
afterEach(() => {
71+
statsdIncrementMock.mockRestore();
6472
});
6573

6674
describe('HTTP requests', () => {
@@ -90,7 +98,7 @@ describe('server', () => {
9098
expect(endMock).toHaveBeenLastCalledWith('OK');
9199
});
92100

93-
test('reponds with a 404 otherwise', () => {
101+
test('reponds with a 404 when not found', () => {
94102
const endMock = jest.fn();
95103
const writeHeadMock = jest.fn();
96104

@@ -168,9 +176,17 @@ describe('server', () => {
168176
const ws = new wsMock('localhost');
169177
const sendMock = jest.spyOn(ws, 'send');
170178
const socketInstance = { ws: ws, channel: channelId, pongTs: Date.now() };
179+
180+
expect(statsdIncrementMock).toBeCalledTimes(0);
171181
server.trackClient(channelId, socketInstance);
182+
expect(statsdIncrementMock).toBeCalledTimes(1);
183+
expect(statsdIncrementMock).toHaveBeenNthCalledWith(
184+
1,
185+
'ws_connected_client',
186+
);
172187

173188
server.processStreamResults(streamReturnValue);
189+
expect(statsdIncrementMock).toBeCalledTimes(1);
174190

175191
const message1 = `{"id":"1615426152415-0","channel_id":"${channelId}","job_id":"c9b99965-8f1e-4ce5-aa43-d6fc94d6a510","user_id":"1","status":"done","errors":[],"result_url":"/superset/explore_json/data/ejr-37281682b1282cdb8f25e0de0339b386"}`;
176192
const message2 = `{"id":"1615426152516-0","channel_id":"${channelId}","job_id":"f1e5bb1f-f2f1-4f21-9b2f-c9b91dcc9b59","user_id":"1","status":"done","errors":[],"result_url":"/api/v1/chart/data/qc-64e8452dc9907dd77746cb75a19202de"}`;
@@ -182,7 +198,9 @@ describe('server', () => {
182198
const ws = new wsMock('localhost');
183199
const sendMock = jest.spyOn(ws, 'send');
184200

201+
expect(statsdIncrementMock).toBeCalledTimes(0);
185202
server.processStreamResults(streamReturnValue);
203+
expect(statsdIncrementMock).toBeCalledTimes(0);
186204

187205
expect(sendMock).not.toHaveBeenCalled();
188206
});
@@ -194,9 +212,21 @@ describe('server', () => {
194212
});
195213
const cleanChannelMock = jest.spyOn(server, 'cleanChannel');
196214
const socketInstance = { ws: ws, channel: channelId, pongTs: Date.now() };
215+
216+
expect(statsdIncrementMock).toBeCalledTimes(0);
197217
server.trackClient(channelId, socketInstance);
218+
expect(statsdIncrementMock).toBeCalledTimes(1);
219+
expect(statsdIncrementMock).toHaveBeenNthCalledWith(
220+
1,
221+
'ws_connected_client',
222+
);
198223

199224
server.processStreamResults(streamReturnValue);
225+
expect(statsdIncrementMock).toBeCalledTimes(2);
226+
expect(statsdIncrementMock).toHaveBeenNthCalledWith(
227+
2,
228+
'ws_client_send_error',
229+
);
200230

201231
expect(sendMock).toHaveBeenCalled();
202232
expect(cleanChannelMock).toHaveBeenCalledWith(channelId);

superset-websocket/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { v4 as uuidv4 } from 'uuid';
2323
import jwt from 'jsonwebtoken';
2424
import cookie from 'cookie';
2525
import Redis from 'ioredis';
26+
import StatsD from 'hot-shots';
2627

2728
import { createLogger } from './logger';
2829

@@ -84,6 +85,11 @@ export const opts = {
8485
logLevel: 'info',
8586
logToFile: false,
8687
logFilename: 'app.log',
88+
statsd: {
89+
host: '127.0.0.1',
90+
port: 8125,
91+
globalTags: [],
92+
},
8793
redis: {
8894
port: 6379,
8995
host: '127.0.0.1',
@@ -121,6 +127,13 @@ const logger = createLogger({
121127
logFilename: opts.logFilename,
122128
});
123129

130+
export const statsd = new StatsD({
131+
...opts.statsd,
132+
errorHandler: (e: Error) => {
133+
logger.error(e);
134+
},
135+
});
136+
124137
// enforce JWT secret length
125138
if (startServer && opts.jwtSecret.length < 32)
126139
throw new Error('Please provide a JWT secret at least 32 bytes long');
@@ -160,6 +173,8 @@ export const trackClient = (
160173
channel: string,
161174
socketInstance: SocketInstance,
162175
): string => {
176+
statsd.increment('ws_connected_client');
177+
163178
const socketId = uuidv4();
164179
sockets[socketId] = socketInstance;
165180

@@ -189,6 +204,7 @@ export const sendToChannel = (channel: string, value: EventValue): void => {
189204
try {
190205
socketInstance.ws.send(strData);
191206
} catch (err) {
207+
statsd.increment('ws_client_send_error');
192208
logger.debug(`Error sending to socket: ${err}`);
193209
// check that the connection is still active
194210
cleanChannel(channel);

0 commit comments

Comments
 (0)