Skip to content

Commit a283138

Browse files
authored
feat: Support env vars configuration for WebSocket server (apache#14398)
1 parent 6541a03 commit a283138

File tree

4 files changed

+207
-38
lines changed

4 files changed

+207
-38
lines changed

superset-websocket/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ npm install
6464

6565
Copy `config.example.json` to `config.json` and adjust the values for your environment.
6666

67+
Configuration via environment variables is also supported which can be helpful in certain contexts, e.g., deployment. `src/config.ts` can be consulted to see the full list of supported values.
68+
6769
## Superset Configuration
6870

6971
Configure the Superset Flask app to enable global async queries (in `superset_config.py`):
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { buildConfig } from '../src/config';
20+
21+
describe('buildConfig', () => {
22+
test('builds configuration and applies env overrides', () => {
23+
let config = buildConfig();
24+
25+
expect(config.jwtSecret).toEqual(
26+
'test123-test123-test123-test123-test123-test123-test123',
27+
);
28+
expect(config.redis.host).toEqual('127.0.0.1');
29+
expect(config.redis.port).toEqual(6379);
30+
expect(config.redis.password).toEqual('');
31+
expect(config.redis.db).toEqual(10);
32+
expect(config.redis.ssl).toEqual(false);
33+
expect(config.statsd.host).toEqual('127.0.0.1');
34+
expect(config.statsd.port).toEqual(8125);
35+
expect(config.statsd.globalTags).toEqual([]);
36+
37+
process.env.JWT_SECRET = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
38+
process.env.REDIS_HOST = '10.10.10.10';
39+
process.env.REDIS_PORT = '6380';
40+
process.env.REDIS_PASSWORD = 'admin';
41+
process.env.REDIS_DB = '4';
42+
process.env.REDIS_SSL = 'true';
43+
process.env.STATSD_HOST = '15.15.15.15';
44+
process.env.STATSD_PORT = '8000';
45+
process.env.STATSD_GLOBAL_TAGS = 'tag-1,tag-2';
46+
47+
config = buildConfig();
48+
49+
expect(config.jwtSecret).toEqual('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
50+
expect(config.redis.host).toEqual('10.10.10.10');
51+
expect(config.redis.port).toEqual(6380);
52+
expect(config.redis.password).toEqual('admin');
53+
expect(config.redis.db).toEqual(4);
54+
expect(config.redis.ssl).toEqual(true);
55+
expect(config.statsd.host).toEqual('15.15.15.15');
56+
expect(config.statsd.port).toEqual(8000);
57+
expect(config.statsd.globalTags).toEqual(['tag-1', 'tag-2']);
58+
59+
delete process.env.JWT_SECRET;
60+
delete process.env.REDIS_HOST;
61+
delete process.env.REDIS_PORT;
62+
delete process.env.REDIS_PASSWORD;
63+
delete process.env.REDIS_DB;
64+
delete process.env.REDIS_SSL;
65+
delete process.env.STATSD_HOST;
66+
delete process.env.STATSD_PORT;
67+
delete process.env.STATSD_GLOBAL_TAGS;
68+
});
69+
});

superset-websocket/src/config.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
type ConfigType = {
20+
port: number;
21+
logLevel: string;
22+
logToFile: boolean;
23+
logFilename: string;
24+
statsd: {
25+
host: string;
26+
port: number;
27+
globalTags: Array<string>;
28+
};
29+
redis: {
30+
port: number;
31+
host: string;
32+
password: string;
33+
db: number;
34+
ssl: boolean;
35+
};
36+
redisStreamPrefix: string;
37+
redisStreamReadCount: number;
38+
redisStreamReadBlockMs: number;
39+
jwtSecret: string;
40+
jwtCookieName: string;
41+
socketResponseTimeoutMs: number;
42+
pingSocketsIntervalMs: number;
43+
gcChannelsIntervalMs: number;
44+
};
45+
46+
function defaultConfig(): ConfigType {
47+
return {
48+
port: 8080,
49+
logLevel: 'info',
50+
logToFile: false,
51+
logFilename: 'app.log',
52+
redisStreamPrefix: 'async-events-',
53+
redisStreamReadCount: 100,
54+
redisStreamReadBlockMs: 5000,
55+
jwtSecret: '',
56+
jwtCookieName: 'async-token',
57+
socketResponseTimeoutMs: 60 * 1000,
58+
pingSocketsIntervalMs: 20 * 1000,
59+
gcChannelsIntervalMs: 120 * 1000,
60+
statsd: {
61+
host: '127.0.0.1',
62+
port: 8125,
63+
globalTags: [],
64+
},
65+
redis: {
66+
host: '127.0.0.1',
67+
port: 6379,
68+
password: '',
69+
db: 0,
70+
ssl: false,
71+
},
72+
};
73+
}
74+
75+
function configFromFile(): Partial<ConfigType> {
76+
const isTest = process.env.NODE_ENV === 'test';
77+
const configFile = isTest ? '../config.test.json' : '../config.json';
78+
try {
79+
return require(configFile);
80+
} catch (err) {
81+
console.warn('config.json file not found');
82+
return {};
83+
}
84+
}
85+
86+
const isPresent = (s: string) => /\S+/.test(s);
87+
const toNumber = Number;
88+
const toBoolean = (s: string) => s.toLowerCase() === 'true';
89+
const toStringArray = (s: string) => s.split(',');
90+
91+
function applyEnvOverrides(config: ConfigType): ConfigType {
92+
const envVarConfigSetter: { [envVar: string]: (val: string) => void } = {
93+
PORT: val => (config.port = toNumber(val)),
94+
LOG_LEVEL: val => (config.logLevel = val),
95+
LOG_TO_FILE: val => (config.logToFile = toBoolean(val)),
96+
LOG_FILENAME: val => (config.logFilename = val),
97+
REDIS_STREAM_PREFIX: val => (config.redisStreamPrefix = val),
98+
REDIS_STREAM_READ_COUNT: val =>
99+
(config.redisStreamReadCount = toNumber(val)),
100+
REDIS_STREAM_READ_BLOCK_MS: val =>
101+
(config.redisStreamReadBlockMs = toNumber(val)),
102+
JWT_SECRET: val => (config.jwtSecret = val),
103+
JWT_COOKIE_NAME: val => (config.jwtCookieName = val),
104+
SOCKET_RESPONSE_TIMEOUT_MS: val =>
105+
(config.socketResponseTimeoutMs = toNumber(val)),
106+
PING_SOCKETS_INTERVAL_MS: val =>
107+
(config.pingSocketsIntervalMs = toNumber(val)),
108+
GC_CHANNELS_INTERVAL_MS: val =>
109+
(config.gcChannelsIntervalMs = toNumber(val)),
110+
REDIS_HOST: val => (config.redis.host = val),
111+
REDIS_PORT: val => (config.redis.port = toNumber(val)),
112+
REDIS_PASSWORD: val => (config.redis.password = val),
113+
REDIS_DB: val => (config.redis.db = toNumber(val)),
114+
REDIS_SSL: val => (config.redis.ssl = toBoolean(val)),
115+
STATSD_HOST: val => (config.statsd.host = val),
116+
STATSD_PORT: val => (config.statsd.port = toNumber(val)),
117+
STATSD_GLOBAL_TAGS: val => (config.statsd.globalTags = toStringArray(val)),
118+
};
119+
120+
for (const [envVar, set] of Object.entries(envVarConfigSetter)) {
121+
const envValue = process.env[envVar];
122+
if (envValue && isPresent(envValue)) {
123+
set(envValue);
124+
}
125+
}
126+
127+
return config;
128+
}
129+
130+
export function buildConfig(): ConfigType {
131+
const config = Object.assign(defaultConfig(), configFromFile());
132+
return applyEnvOverrides(config);
133+
}

superset-websocket/src/index.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Redis from 'ioredis';
2626
import StatsD from 'hot-shots';
2727

2828
import { createLogger } from './logger';
29+
import { buildConfig } from './config';
2930

3031
export type StreamResult = [
3132
recordId: string,
@@ -79,45 +80,9 @@ interface ChannelValue {
7980

8081
const environment = process.env.NODE_ENV;
8182

82-
// default options
83-
export const opts = {
84-
port: 8080,
85-
logLevel: 'info',
86-
logToFile: false,
87-
logFilename: 'app.log',
88-
statsd: {
89-
host: '127.0.0.1',
90-
port: 8125,
91-
globalTags: [],
92-
},
93-
redis: {
94-
port: 6379,
95-
host: '127.0.0.1',
96-
password: '',
97-
db: 0,
98-
ssl: false,
99-
},
100-
redisStreamPrefix: 'async-events-',
101-
redisStreamReadCount: 100,
102-
redisStreamReadBlockMs: 5000,
103-
jwtSecret: '',
104-
jwtCookieName: 'async-token',
105-
socketResponseTimeoutMs: 60 * 1000,
106-
pingSocketsIntervalMs: 20 * 1000,
107-
gcChannelsIntervalMs: 120 * 1000,
108-
};
109-
11083
const startServer = process.argv[2] === 'start';
111-
const configFile =
112-
environment === 'test' ? '../config.test.json' : '../config.json';
113-
let config = {};
114-
try {
115-
config = require(configFile);
116-
} catch (err) {
117-
console.error('config.json not found, using defaults');
118-
}
119-
// apply config overrides
120-
Object.assign(opts, config);
84+
85+
export const opts = buildConfig();
12186

12287
// init logger
12388
const logger = createLogger({

0 commit comments

Comments
 (0)