Skip to content

Commit

Permalink
feat: Support env vars configuration for WebSocket server (apache#14398)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjreinhart authored Apr 29, 2021
1 parent fe81880 commit c7f8623
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 38 deletions.
2 changes: 2 additions & 0 deletions superset-websocket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ npm install

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

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.

## Superset Configuration

Configure the Superset Flask app to enable global async queries (in `superset_config.py`):
Expand Down
69 changes: 69 additions & 0 deletions superset-websocket/spec/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildConfig } from '../src/config';

describe('buildConfig', () => {
test('builds configuration and applies env overrides', () => {
let config = buildConfig();

expect(config.jwtSecret).toEqual(
'test123-test123-test123-test123-test123-test123-test123',
);
expect(config.redis.host).toEqual('127.0.0.1');
expect(config.redis.port).toEqual(6379);
expect(config.redis.password).toEqual('');
expect(config.redis.db).toEqual(10);
expect(config.redis.ssl).toEqual(false);
expect(config.statsd.host).toEqual('127.0.0.1');
expect(config.statsd.port).toEqual(8125);
expect(config.statsd.globalTags).toEqual([]);

process.env.JWT_SECRET = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
process.env.REDIS_HOST = '10.10.10.10';
process.env.REDIS_PORT = '6380';
process.env.REDIS_PASSWORD = 'admin';
process.env.REDIS_DB = '4';
process.env.REDIS_SSL = 'true';
process.env.STATSD_HOST = '15.15.15.15';
process.env.STATSD_PORT = '8000';
process.env.STATSD_GLOBAL_TAGS = 'tag-1,tag-2';

config = buildConfig();

expect(config.jwtSecret).toEqual('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
expect(config.redis.host).toEqual('10.10.10.10');
expect(config.redis.port).toEqual(6380);
expect(config.redis.password).toEqual('admin');
expect(config.redis.db).toEqual(4);
expect(config.redis.ssl).toEqual(true);
expect(config.statsd.host).toEqual('15.15.15.15');
expect(config.statsd.port).toEqual(8000);
expect(config.statsd.globalTags).toEqual(['tag-1', 'tag-2']);

delete process.env.JWT_SECRET;
delete process.env.REDIS_HOST;
delete process.env.REDIS_PORT;
delete process.env.REDIS_PASSWORD;
delete process.env.REDIS_DB;
delete process.env.REDIS_SSL;
delete process.env.STATSD_HOST;
delete process.env.STATSD_PORT;
delete process.env.STATSD_GLOBAL_TAGS;
});
});
133 changes: 133 additions & 0 deletions superset-websocket/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
type ConfigType = {
port: number;
logLevel: string;
logToFile: boolean;
logFilename: string;
statsd: {
host: string;
port: number;
globalTags: Array<string>;
};
redis: {
port: number;
host: string;
password: string;
db: number;
ssl: boolean;
};
redisStreamPrefix: string;
redisStreamReadCount: number;
redisStreamReadBlockMs: number;
jwtSecret: string;
jwtCookieName: string;
socketResponseTimeoutMs: number;
pingSocketsIntervalMs: number;
gcChannelsIntervalMs: number;
};

function defaultConfig(): ConfigType {
return {
port: 8080,
logLevel: 'info',
logToFile: false,
logFilename: 'app.log',
redisStreamPrefix: 'async-events-',
redisStreamReadCount: 100,
redisStreamReadBlockMs: 5000,
jwtSecret: '',
jwtCookieName: 'async-token',
socketResponseTimeoutMs: 60 * 1000,
pingSocketsIntervalMs: 20 * 1000,
gcChannelsIntervalMs: 120 * 1000,
statsd: {
host: '127.0.0.1',
port: 8125,
globalTags: [],
},
redis: {
host: '127.0.0.1',
port: 6379,
password: '',
db: 0,
ssl: false,
},
};
}

function configFromFile(): Partial<ConfigType> {
const isTest = process.env.NODE_ENV === 'test';
const configFile = isTest ? '../config.test.json' : '../config.json';
try {
return require(configFile);
} catch (err) {
console.warn('config.json file not found');
return {};
}
}

const isPresent = (s: string) => /\S+/.test(s);
const toNumber = Number;
const toBoolean = (s: string) => s.toLowerCase() === 'true';
const toStringArray = (s: string) => s.split(',');

function applyEnvOverrides(config: ConfigType): ConfigType {
const envVarConfigSetter: { [envVar: string]: (val: string) => void } = {
PORT: val => (config.port = toNumber(val)),
LOG_LEVEL: val => (config.logLevel = val),
LOG_TO_FILE: val => (config.logToFile = toBoolean(val)),
LOG_FILENAME: val => (config.logFilename = val),
REDIS_STREAM_PREFIX: val => (config.redisStreamPrefix = val),
REDIS_STREAM_READ_COUNT: val =>
(config.redisStreamReadCount = toNumber(val)),
REDIS_STREAM_READ_BLOCK_MS: val =>
(config.redisStreamReadBlockMs = toNumber(val)),
JWT_SECRET: val => (config.jwtSecret = val),
JWT_COOKIE_NAME: val => (config.jwtCookieName = val),
SOCKET_RESPONSE_TIMEOUT_MS: val =>
(config.socketResponseTimeoutMs = toNumber(val)),
PING_SOCKETS_INTERVAL_MS: val =>
(config.pingSocketsIntervalMs = toNumber(val)),
GC_CHANNELS_INTERVAL_MS: val =>
(config.gcChannelsIntervalMs = toNumber(val)),
REDIS_HOST: val => (config.redis.host = val),
REDIS_PORT: val => (config.redis.port = toNumber(val)),
REDIS_PASSWORD: val => (config.redis.password = val),
REDIS_DB: val => (config.redis.db = toNumber(val)),
REDIS_SSL: val => (config.redis.ssl = toBoolean(val)),
STATSD_HOST: val => (config.statsd.host = val),
STATSD_PORT: val => (config.statsd.port = toNumber(val)),
STATSD_GLOBAL_TAGS: val => (config.statsd.globalTags = toStringArray(val)),
};

for (const [envVar, set] of Object.entries(envVarConfigSetter)) {
const envValue = process.env[envVar];
if (envValue && isPresent(envValue)) {
set(envValue);
}
}

return config;
}

export function buildConfig(): ConfigType {
const config = Object.assign(defaultConfig(), configFromFile());
return applyEnvOverrides(config);
}
41 changes: 3 additions & 38 deletions superset-websocket/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Redis from 'ioredis';
import StatsD from 'hot-shots';

import { createLogger } from './logger';
import { buildConfig } from './config';

export type StreamResult = [
recordId: string,
Expand Down Expand Up @@ -79,45 +80,9 @@ interface ChannelValue {

const environment = process.env.NODE_ENV;

// default options
export const opts = {
port: 8080,
logLevel: 'info',
logToFile: false,
logFilename: 'app.log',
statsd: {
host: '127.0.0.1',
port: 8125,
globalTags: [],
},
redis: {
port: 6379,
host: '127.0.0.1',
password: '',
db: 0,
ssl: false,
},
redisStreamPrefix: 'async-events-',
redisStreamReadCount: 100,
redisStreamReadBlockMs: 5000,
jwtSecret: '',
jwtCookieName: 'async-token',
socketResponseTimeoutMs: 60 * 1000,
pingSocketsIntervalMs: 20 * 1000,
gcChannelsIntervalMs: 120 * 1000,
};

const startServer = process.argv[2] === 'start';
const configFile =
environment === 'test' ? '../config.test.json' : '../config.json';
let config = {};
try {
config = require(configFile);
} catch (err) {
console.error('config.json not found, using defaults');
}
// apply config overrides
Object.assign(opts, config);

export const opts = buildConfig();

// init logger
const logger = createLogger({
Expand Down

0 comments on commit c7f8623

Please sign in to comment.