CartoDB-SQL-API/lib/api/sql/query-controller.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

129 lines
4.5 KiB
JavaScript

'use strict';
const bodyParser = require('../middlewares/body-parser');
const user = require('../middlewares/user');
const rateLimits = require('../middlewares/rate-limit');
const authorization = require('../middlewares/authorization');
const connectionParams = require('../middlewares/connection-params');
const timeoutLimits = require('../middlewares/timeout-limits');
const params = require('../middlewares/params');
const log = require('../middlewares/log-query');
const cancelOnClientAbort = require('../middlewares/cancel-on-client-abort');
const affectedTables = require('../middlewares/affected-tables');
const accessValidator = require('../middlewares/access-validator');
const queryMayWrite = require('../middlewares/query-may-write');
const cacheControl = require('../middlewares/cache-control');
const cacheChannel = require('../middlewares/cache-channel');
const surrogateKey = require('../middlewares/surrogate-key');
const lastModified = require('../middlewares/last-modified');
const formatter = require('../middlewares/formatter');
const content = require('../middlewares/content');
const tag = require('../middlewares/tag');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimits;
const PSQL = require('cartodb-psql');
module.exports = class QueryController {
constructor (metadataBackend, userDatabaseService, statsdClient, userLimitsService) {
this.metadataBackend = metadataBackend;
this.stats = statsdClient;
this.userDatabaseService = userDatabaseService;
this.userLimitsService = userLimitsService;
}
route (sqlRouter) {
const forceToBeMaster = false;
const queryMiddlewares = () => {
return [
tag({ tags: ['query'] }),
bodyParser(),
user(this.metadataBackend),
rateLimits(this.userLimitsService, RATE_LIMIT_ENDPOINTS_GROUPS.QUERY),
authorization(this.metadataBackend, forceToBeMaster),
connectionParams(this.userDatabaseService),
timeoutLimits(this.metadataBackend),
params({ strategy: 'query' }),
log(),
cancelOnClientAbort(),
affectedTables(),
accessValidator(),
queryMayWrite(),
cacheControl(),
cacheChannel(),
surrogateKey(),
lastModified(),
formatter(),
content(),
handleQuery({ stats: this.stats })
];
};
sqlRouter.all('/', queryMiddlewares());
sqlRouter.all('.:f', queryMiddlewares());
}
};
function handleQuery ({ stats } = {}) {
return function handleQueryMiddleware (req, res, next) {
const { user: username, userDbParams: dbopts, userLimits } = res.locals;
const { orderBy, sortOrder, limit, offset } = res.locals.params;
const { sql, skipfields, decimalPrecision, filename, callback } = res.locals.params;
let { formatter } = req;
try {
req.profiler.done('init');
const opts = {
username: username,
dbopts: dbopts,
sink: res,
gn: 'the_geom', // TODO: read from configuration FILE,
dp: decimalPrecision,
skipfields: skipfields,
sql: new PSQL.QueryWrapper(sql).orderBy(orderBy, sortOrder).window(limit, offset).query(),
filename: filename,
bufferedRows: global.settings.bufferedRows,
callback: callback,
timeout: userLimits.timeout
};
if (req.profiler) {
opts.profiler = req.profiler;
opts.beforeSink = function () {
req.profiler.done('beforeSink');
};
}
if (dbopts.host) {
res.header('X-Served-By-DB-Host', dbopts.host);
}
formatter.sendResponse(opts, (err) => {
formatter = null;
if (stats) {
if (err) {
stats.increment('sqlapi.query.error');
} else {
stats.increment('sqlapi.query.success');
}
}
if (err) {
next(err);
} else {
next();
}
});
} catch (err) {
next(err);
if (stats) {
stats.increment('sqlapi.query.error');
}
}
};
}