CartoDB-SQL-API/app.js
Daniel García Aubert 762a240890 Breaking changes:
- Log system revamp:
  - Logs to stdout, disabled while testing
  - Use header `X-Request-Id`, or create a new `uuid` when no present, to identyfy log entries
  - Be able to set log level from env variable `LOG_LEVEL`, useful while testing: `LOG_LEVEL=info npm test`; even more human-readable: `LOG_LEVEL=info npm t | ./node_modules/.bin/pino-pretty`
  - Be able to reduce the footprint in the final log file depending on the environment
  - Use one logger for every service: Queries, Batch Queries (Jobs), and Data Ingestion (CopyTo/CopyFrom)
  - Stop using headers such as: `X-SQL-API-Log`, `X-SQL-API-Profiler`, and `X-SQL-API-Errors` as a way to log info.
  - Be able to tag requests with labels as an easier way to provide business metrics
  - Metro: Add log-collector utility (`metro`), it will be moved to its own repository. Attaching it here fro development purposes. Try it with the following command `LOG_LEVEL=info npm t | node metro`
  - Metro: Creates `metrics-collector.js` a stream to update Prometheus' counters and histograms and exposes them via Express' app (`:9145/metrics`). Use the ones defined in `grok_exporter`

Announcements:
- Profiler is always set. No need to check its existence anymore
- Unify profiler usage for every endpoint

Bug fixes:
- Avoid hung requests while fetching user identifier
2020-06-30 17:42:59 +02:00

224 lines
6.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const fqdn = require('@carto/fqdn-sync');
const serverOptions = require('./lib/server-options');
const { logger } = serverOptions();
const argv = require('yargs')
.usage('Usage: node $0 <environment> [options]')
.help('h')
.example(
'node $0 production -c /etc/sql-api/config.js',
'start server in production environment with /etc/sql-api/config.js as config file'
)
.alias('h', 'help')
.alias('c', 'config')
.nargs('c', 1)
.describe('c', 'Load configuration from path')
.argv;
const environmentArg = argv._[0] || process.env.NODE_ENV || 'development';
const configurationFile = path.resolve(argv.config || './config/environments/' + environmentArg + '.js');
if (!fs.existsSync(configurationFile)) {
logger.fatal(new Error(`Configuration file "${configurationFile}" does not exist`));
process.exit(1);
}
global.settings = require(configurationFile);
const ENVIRONMENT = argv._[0] || process.env.NODE_ENV || global.settings.environment;
process.env.NODE_ENV = ENVIRONMENT;
const availableEnvironments = ['development', 'production', 'test', 'staging'];
if (!availableEnvironments.includes(ENVIRONMENT)) {
logger.fatal(new Error(`Invalid environment argument, valid ones: ${Object.keys(availableEnvironments).join(', ')}`));
process.exit(1);
}
global.settings.api_hostname = fqdn.hostname();
const StatsClient = require('./lib/stats/client');
if (global.settings.statsd) {
// Perform keyword substitution in statsd
if (global.settings.statsd.prefix) {
global.settings.statsd.prefix = global.settings.statsd.prefix.replace(/:host/, fqdn.reverse());
}
}
const statsClient = StatsClient.getInstance(global.settings.statsd);
const { version, name } = require('./package');
const createServer = require('./lib/server');
const server = createServer(statsClient);
const listener = server.listen(global.settings.node_port, global.settings.node_host);
listener.on('listening', function () {
const { address, port } = listener.address();
logger.info({ 'Node.js': process.version, pid: process.pid, environment: process.env.NODE_ENV, [name]: version, address, port, config: configurationFile }, `${name} initialized successfully`);
});
process.on('uncaughtException', function (err) {
logger.error(err, 'Uncaught exception');
});
const exitProcess = logger.finish((err, finalLogger, listener, signal, killTimeout) => {
scheduleForcedExit(killTimeout, finalLogger);
finalLogger.info(`Process has received signal: ${signal}`);
let code = 0;
if (err) {
code = 1;
finalLogger.fatal(err);
}
finalLogger.info(`Process is going to exit with code: ${code}`);
listener.close(() => process.exit(code));
listener.close(() => {
server.batch.stop(() => {
server.batch.drain((err) => {
if (err) {
finalLogger.error(err);
return process.exit(1);
}
process.exit(code);
});
});
});
});
function addHandlers (listener, killTimeout) {
// FIXME: minimize the number of 'uncaughtException' before uncomment the following line
// process.on('uncaughtException', (err) => exitProcess(err, listener, 'uncaughtException', killTimeout));
process.on('unhandledRejection', (err) => exitProcess(err, listener, 'unhandledRejection', killTimeout));
process.on('ENOMEM', (err) => exitProcess(err, listener, 'ENOMEM', killTimeout));
process.on('SIGINT', () => exitProcess(null, listener, 'SIGINT', killTimeout));
process.on('SIGTERM', () => exitProcess(null, listener, 'SIGINT', killTimeout));
}
addHandlers(listener, 45000);
function scheduleForcedExit (killTimeout, finalLogger) {
// Schedule exit if there is still ongoing work to deal with
const killTimer = setTimeout(() => {
finalLogger.info('Process didn\'t close on time. Force exit');
process.exit(1);
}, killTimeout);
// Don't keep the process open just for this
killTimer.unref();
}
function isGteMinVersion (version, minVersion) {
const versionMatch = /[a-z]?([0-9]*)/.exec(version);
if (versionMatch) {
const majorVersion = parseInt(versionMatch[1], 10);
if (Number.isFinite(majorVersion)) {
return majorVersion >= minVersion;
}
}
return false;
}
setInterval(function memoryUsageMetrics () {
const memoryUsage = process.memoryUsage();
Object.keys(memoryUsage).forEach(property => {
statsClient.gauge(`sqlapi.memory.${property}`, memoryUsage[property]);
});
}, 5000);
function getCPUUsage (oldUsage) {
let usage;
if (oldUsage && oldUsage._start) {
usage = Object.assign({}, process.cpuUsage(oldUsage._start.cpuUsage));
usage.time = Date.now() - oldUsage._start.time;
} else {
usage = Object.assign({}, process.cpuUsage());
usage.time = process.uptime() * 1000; // s to ms
}
usage.percent = (usage.system + usage.user) / (usage.time * 10);
Object.defineProperty(usage, '_start', {
value: {
cpuUsage: process.cpuUsage(),
time: Date.now()
}
});
return usage;
}
let previousCPUUsage = getCPUUsage();
setInterval(function cpuUsageMetrics () {
const CPUUsage = getCPUUsage(previousCPUUsage);
Object.keys(CPUUsage).forEach(property => {
statsClient.gauge(`sqlapi.cpu.${property}`, CPUUsage[property]);
});
previousCPUUsage = CPUUsage;
}, 5000);
if (global.gc && isGteMinVersion(process.version, 6)) {
const gcInterval = Number.isFinite(global.settings.gc_interval)
? global.settings.gc_interval
: 10000;
if (gcInterval > 0) {
setInterval(function gcForcedCycle () {
global.gc();
}, gcInterval);
}
}
const gcStats = require('gc-stats')();
gcStats.on('stats', function ({ pauseMS, gctype }) {
statsClient.timing('sqlapi.gc', pauseMS);
statsClient.timing(`sqlapi.gctype.${getGCTypeValue(gctype)}`, pauseMS);
});
function getGCTypeValue (type) {
// 1: Scavenge (minor GC)
// 2: Mark/Sweep/Compact (major GC)
// 4: Incremental marking
// 8: Weak/Phantom callback processing
// 15: All
let value;
switch (type) {
case 1:
value = 'Scavenge';
break;
case 2:
value = 'MarkSweepCompact';
break;
case 4:
value = 'IncrementalMarking';
break;
case 8:
value = 'ProcessWeakCallbacks';
break;
case 15:
value = 'All';
break;
default:
value = 'Unkown';
break;
}
return value;
}