You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/09 16:33:28 UTC

(superset) branch master updated: feat(websocket): improve operational logging and crash safety (#40868)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 62084f4015e feat(websocket): improve operational logging and crash safety (#40868)
62084f4015e is described below

commit 62084f4015ed0d6e97147c5da1559425a9c4e0f5
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Tue Jun 9 09:33:13 2026 -0700

    feat(websocket): improve operational logging and crash safety (#40868)
    
    Co-authored-by: Claude Code <no...@anthropic.com>
---
 superset-websocket/spec/index.test.ts |  4 ++-
 superset-websocket/src/index.ts       | 47 +++++++++++++++++++++++++++++------
 superset-websocket/src/logger.ts      |  1 +
 3 files changed, 43 insertions(+), 9 deletions(-)

diff --git a/superset-websocket/spec/index.test.ts b/superset-websocket/spec/index.test.ts
index ed306260974..bb7d3151175 100644
--- a/superset-websocket/spec/index.test.ts
+++ b/superset-websocket/spec/index.test.ts
@@ -43,7 +43,7 @@ const mockRedisXrange = jest.fn() as jest.MockedFunction<MockedRedisXrange>;
 jest.mock('ws');
 jest.mock('ioredis', () => {
   return jest.fn().mockImplementation(() => {
-    return { xrange: mockRedisXrange };
+    return { xrange: mockRedisXrange, on: jest.fn() };
   });
 });
 
@@ -526,6 +526,8 @@ describe('server', () => {
       server.httpUpgrade(request, socket, Buffer.alloc(5));
       expect(socketDestroySpy).toHaveBeenCalled();
       expect(wssUpgradeSpy).not.toHaveBeenCalled();
+      // rejected upgrades are counted for auditability
+      expect(statsdIncrementMock).toHaveBeenCalledWith('ws_upgrade_rejected');
     });
 
     test('valid JWT, no channel', async () => {
diff --git a/superset-websocket/src/index.ts b/superset-websocket/src/index.ts
index bebc89a20ec..38e09521b1d 100644
--- a/superset-websocket/src/index.ts
+++ b/superset-websocket/src/index.ts
@@ -18,6 +18,7 @@
  */
 import * as http from 'http';
 import * as net from 'net';
+import { inspect } from 'util';
 import WebSocket, { WebSocketServer } from 'ws';
 import { randomUUID } from 'crypto';
 import jwt, { Algorithm } from 'jsonwebtoken';
@@ -96,15 +97,15 @@ export const statsd = new StatsD({
 
 // enforce JWT secret length
 if (startServer && opts.jwtSecret.length < 32) {
-  console.error('ERROR: Please provide a JWT secret at least 32 bytes long');
+  logger.error('Please provide a JWT secret at least 32 bytes long');
   process.exit(1);
 }
 
 if (startServer && opts.jwtSecret.startsWith('CHANGE-ME')) {
-  console.warn(
-    'WARNING: it appears your secret in your config.json is insecure',
+  logger.warn(
+    'It appears your secret in your config.json is insecure. ' +
+      'DO NOT USE IN PRODUCTION',
   );
-  console.warn('DO NOT USE IN PRODUCTION');
 }
 
 export const buildRedisOpts = (baseConfig: RedisConfig) => {
@@ -140,6 +141,9 @@ export const buildRedisOpts = (baseConfig: RedisConfig) => {
 
 // initialize servers
 const redis = new Redis(buildRedisOpts(opts.redis));
+redis.on('error', (err: Error) => {
+  logger.error(`Redis connection error: ${err.message}`);
+});
 const httpServer = http.createServer();
 export const wss = new WebSocketServer({
   noServer: true,
@@ -267,7 +271,9 @@ export const subscribeToGlobalStream = async (
  * Callback function to process events received from a Redis Stream
  */
 export const processStreamResults = (results: StreamResult[]): void => {
-  logger.debug(`events received: ${results}`);
+  // Log only the batch size, not the raw payloads, which carry user and
+  // job identifiers.
+  logger.debug(`events received: count=${results.length}`);
   results.forEach(item => {
     try {
       const id = item[0];
@@ -437,8 +443,14 @@ export const httpUpgrade = (
   try {
     readChannelId(request);
   } catch (err) {
-    // JWT invalid, do not establish a WebSocket connection
-    logger.error(err);
+    // Token invalid/absent: do not establish a WebSocket connection. Record a
+    // structured warning (with the request's remote address) so rejected
+    // upgrade attempts are auditable, without logging the token itself.
+    statsd.increment('ws_upgrade_rejected');
+    logger.warn(
+      `Rejected WebSocket upgrade from ${request.socket.remoteAddress ?? 'unknown'}: ` +
+        `${(err as Error).message}`,
+    );
     socket.destroy();
     return;
   }
@@ -513,9 +525,28 @@ export const cleanChannel = (channel: string) => {
 // server startup
 
 if (startServer) {
+  // Last-resort handlers so an unhandled async error is recorded through the
+  // configured logger instead of printing a default trace (or, for an
+  // unhandled rejection, terminating the process on newer Node versions).
+  process.on('unhandledRejection', (reason: unknown) => {
+    // Normalize the reason defensively: a raw template interpolation throws on
+    // a Symbol (or other exotic value), which would crash this last-resort
+    // handler. `inspect` safely stringifies any value.
+    logger.error(`Unhandled promise rejection: ${inspect(reason)}`);
+  });
+  process.on('uncaughtException', (err: unknown) => {
+    // JavaScript can throw non-Error values (including null), so guard the
+    // shape before dereferencing instead of assuming an Error is present.
+    const detail =
+      err instanceof Error ? (err.stack ?? err.message) : inspect(err);
+    logger.error(`Uncaught exception: ${detail}`);
+  });
+
   // init server event listeners
   wss.on('connection', function (ws: WebSocket) {
-    ws.on('error', console.error);
+    ws.on('error', (err: Error) =>
+      logger.error(`socket error: ${err.message}`),
+    );
   });
   wss.on('connection', wsConnection);
   httpServer.on('request', httpRequest);
diff --git a/superset-websocket/src/logger.ts b/superset-websocket/src/logger.ts
index c6efc2a5043..96ce6dda3d3 100644
--- a/superset-websocket/src/logger.ts
+++ b/superset-websocket/src/logger.ts
@@ -43,6 +43,7 @@ export function createLogger(opts: LoggingOptionsType) {
     level: opts.logLevel,
     transports: logTransports,
     format: winston.format.combine(
+      winston.format.timestamp(),
       winston.format.errors({ stack: true }),
       winston.format.json(),
     ),