Merge pull request #1183 from CartoDB/feature/ch91877/remove-log-aggregation-in-metro
Metro: stop aggregating logs per request id
This commit is contained in:
commit
d5c5d07507
2
app.js
2
app.js
@ -198,7 +198,7 @@ function addHandlers (listener, killTimeout) {
|
|||||||
process.on('unhandledRejection', (err) => exitProcess(err, listener, 'unhandledRejection', killTimeout));
|
process.on('unhandledRejection', (err) => exitProcess(err, listener, 'unhandledRejection', killTimeout));
|
||||||
process.on('ENOMEM', (err) => exitProcess(err, listener, 'ENOMEM', killTimeout));
|
process.on('ENOMEM', (err) => exitProcess(err, listener, 'ENOMEM', killTimeout));
|
||||||
process.on('SIGINT', () => exitProcess(null, listener, 'SIGINT', killTimeout));
|
process.on('SIGINT', () => exitProcess(null, listener, 'SIGINT', killTimeout));
|
||||||
process.on('SIGTERM', () => exitProcess(null, listener, 'SIGINT', killTimeout));
|
process.on('SIGTERM', () => exitProcess(null, listener, 'SIGTERM', killTimeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
addHandlers(listener, 45000);
|
addHandlers(listener, 45000);
|
||||||
|
@ -199,12 +199,12 @@ module.exports = class ApiRouter {
|
|||||||
const apiRouter = router({ mergeParams: true });
|
const apiRouter = router({ mergeParams: true });
|
||||||
const { paths, middlewares = [] } = route;
|
const { paths, middlewares = [] } = route;
|
||||||
|
|
||||||
|
apiRouter.use(user(this.metadataBackend));
|
||||||
apiRouter.use(initLogger({ logger: this.serverOptions.logger }));
|
apiRouter.use(initLogger({ logger: this.serverOptions.logger }));
|
||||||
apiRouter.use(profiler({
|
apiRouter.use(profiler({
|
||||||
enabled: this.serverOptions.useProfiler,
|
enabled: this.serverOptions.useProfiler,
|
||||||
statsClient: global.statsClient
|
statsClient: global.statsClient
|
||||||
}));
|
}));
|
||||||
apiRouter.use(user(this.metadataBackend));
|
|
||||||
|
|
||||||
middlewares.forEach(middleware => apiRouter.use(middleware()));
|
middlewares.forEach(middleware => apiRouter.use(middleware()));
|
||||||
|
|
||||||
@ -236,10 +236,10 @@ function createTemplateMaps ({ redisPool, surrogateKeysCache, logger }) {
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(user, templateName), (err) => {
|
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(user, templateName), (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return logger.error(err, `Named map (${templateName}) invalidation failed, user: ${user}`);
|
return logger.error({ exception: err, 'cdb-user': user, template_id: templateName }, 'Named map invalidation failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info({ user, type: 'named_map_invalidation', elapsed: Date.now() - startTime }, `Named map (${templateName}) invalidation success, user: ${user}`);
|
logger.info({ 'cdb-user': user, template_id: templateName, duration: (Date.now() - startTime) / 1000 }, 'Named map invalidation success');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,8 +343,7 @@ function incrementMapViews ({ metadataBackend }) {
|
|||||||
|
|
||||||
mapConfigProvider.getMapConfig((err, mapConfig) => {
|
mapConfigProvider.getMapConfig((err, mapConfig) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Failed to increment mapview count for user '${user}'. ${err.message}`;
|
logger.warn({ exception: err }, 'Failed to increment mapview count');
|
||||||
logger.warn({ error: err });
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,8 +353,7 @@ function incrementMapViews ({ metadataBackend }) {
|
|||||||
|
|
||||||
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Failed to increment mapview count for user '${user}'. ${err.message}`;
|
logger.warn({ exception: err }, 'Failed to increment mapview count');
|
||||||
logger.warn({ error: err });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
@ -10,8 +10,7 @@ module.exports = function setCacheChannelHeader () {
|
|||||||
|
|
||||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Error generating Cache Channel Header. ${err.message}`;
|
logger.warn({ exception: err }, 'Error generating Cache Channel Header');
|
||||||
logger.warn({ error: err });
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +44,7 @@ module.exports = function setCacheControlHeader ({
|
|||||||
|
|
||||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Error generating Cache Control Header. ${err.message}`;
|
logger.warn({ exception: err }, 'Error generating Cache Control Header');
|
||||||
logger.warn({ error: err });
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ module.exports = function errorMiddleware (/* options */) {
|
|||||||
const { logger } = res.locals;
|
const { logger } = res.locals;
|
||||||
const errors = populateLimitErrors(Array.isArray(err) ? err : [err]);
|
const errors = populateLimitErrors(Array.isArray(err) ? err : [err]);
|
||||||
|
|
||||||
logger.error({ error: errors });
|
errors.forEach((err) => logger.error({ exception: err }, 'Error while handling the request'));
|
||||||
|
|
||||||
setCommonHeaders(req, res, () => {
|
setCommonHeaders(req, res, () => {
|
||||||
const errorResponseBody = {
|
const errorResponseBody = {
|
||||||
|
@ -7,8 +7,7 @@ module.exports = function incrementMapViewCount (metadataBackend) {
|
|||||||
|
|
||||||
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Failed to increment mapview count for user '${user}'. ${err.message}`;
|
logger.warn({ exception: err }, 'Failed to increment mapview count');
|
||||||
logger.warn({ error: err });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
@ -21,8 +21,7 @@ module.exports = function setLastModifiedHeader () {
|
|||||||
|
|
||||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Error generating Last Modified Header. ${err.message}`;
|
logger.warn({ exception: err }, 'Error generating Last Modified Header');
|
||||||
logger.warn({ error: err });
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,13 +4,9 @@ const uuid = require('uuid');
|
|||||||
|
|
||||||
module.exports = function initLogger ({ logger }) {
|
module.exports = function initLogger ({ logger }) {
|
||||||
return function initLoggerMiddleware (req, res, next) {
|
return function initLoggerMiddleware (req, res, next) {
|
||||||
const id = req.get('X-Request-Id') || uuid.v4();
|
res.locals.logger = logger.child({ request_id: req.get('X-Request-Id') || uuid.v4(), 'cdb-user': res.locals.user });
|
||||||
res.locals.logger = logger.child({ id });
|
res.locals.logger.info({ client_request: req }, 'Incoming request');
|
||||||
|
res.on('finish', () => res.locals.logger.info({ server_response: res, status: res.statusCode }, 'Response sent'));
|
||||||
res.locals.logger.info({ request: req });
|
|
||||||
res.on('finish', () => res.locals.logger.info({ response: res }));
|
|
||||||
res.on('close', () => res.locals.logger.info({ end: true }));
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@ module.exports = function metrics ({ enabled, tags, metricsBackend }) {
|
|||||||
const { event, attributes } = getEventData(req, res, tags);
|
const { event, attributes } = getEventData(req, res, tags);
|
||||||
|
|
||||||
metricsBackend.send(event, attributes)
|
metricsBackend.send(event, attributes)
|
||||||
.catch((err) => logger.error(err, `Failed to publish event "${event}"`));
|
.catch((err) => logger.error({ exception: err, event }, 'Failed to publish event'));
|
||||||
});
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
@ -21,7 +21,8 @@ module.exports = function profiler (options) {
|
|||||||
res.on('finish', () => {
|
res.on('finish', () => {
|
||||||
req.profiler.done('response');
|
req.profiler.done('response');
|
||||||
req.profiler.end();
|
req.profiler.end();
|
||||||
logger.info({ stats: req.profiler.toJSON() });
|
const stats = req.profiler.toJSON();
|
||||||
|
logger.info({ stats, duration: stats.response / 1000 }, 'Request profiling stats');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166
|
// May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166
|
||||||
|
@ -17,8 +17,7 @@ module.exports = function setSurrogateKeyHeader ({ surrogateKeysCache }) {
|
|||||||
|
|
||||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Erros generating Surrogate Key Header. ${err.message}`;
|
logger.warn({ exception: err }, 'Error generating Surrogate Key Header');
|
||||||
logger.warn({ error: err });
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ module.exports = function tag ({ tags }) {
|
|||||||
return function tagMiddleware (req, res, next) {
|
return function tagMiddleware (req, res, next) {
|
||||||
const { logger } = res.locals;
|
const { logger } = res.locals;
|
||||||
res.locals.tags = tags;
|
res.locals.tags = tags;
|
||||||
res.on('finish', () => logger.info({ tags: res.locals.tags }));
|
res.on('finish', () => logger.info({ tags: res.locals.tags }, 'Request tagged'));
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
@ -6,10 +6,8 @@ module.exports = function user (metadataBackend) {
|
|||||||
const cdbRequest = new CdbRequest();
|
const cdbRequest = new CdbRequest();
|
||||||
|
|
||||||
return function userMiddleware (req, res, next) {
|
return function userMiddleware (req, res, next) {
|
||||||
const { logger } = res.locals;
|
|
||||||
try {
|
try {
|
||||||
res.locals.user = getUserNameFromRequest(req, cdbRequest);
|
res.locals.user = getUserNameFromRequest(req, cdbRequest);
|
||||||
logger.info({ user: res.locals.user });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
module.exports = function setCommonHeaders (req, res, callback) {
|
module.exports = function setCommonHeaders (req, res, callback) {
|
||||||
const { logger } = res.locals;
|
const { logger } = res.locals;
|
||||||
|
|
||||||
res.set('X-Request-Id', logger.bindings().id);
|
res.set('X-Request-Id', logger.bindings().request_id);
|
||||||
|
|
||||||
// TODO: x-layergroupid header??
|
// TODO: x-layergroupid header??
|
||||||
|
|
||||||
@ -39,8 +39,7 @@ module.exports = function setCommonHeaders (req, res, callback) {
|
|||||||
|
|
||||||
getStatTag({ res }, (err, statTag) => {
|
getStatTag({ res }, (err, statTag) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
err.message = `Error generating Stat Tag header: ${err.message}`;
|
logger.warn({ exception: err }, 'Error generating Stat Tag header');
|
||||||
logger.warn({ error: err });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statTag) {
|
if (statTag) {
|
||||||
@ -100,8 +99,7 @@ function getTemplateHash ({ res }) {
|
|||||||
try {
|
try {
|
||||||
templateHash = res.locals.mapConfigProvider.getTemplateHash().substring(0, 8);
|
templateHash = res.locals.mapConfigProvider.getTemplateHash().substring(0, 8);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
err.message = `Error generating Stat Tag header: ${err.message}`;
|
logger.warn({ exception: err }, 'Error generating Stat Tag header');
|
||||||
logger.warn({ error: err });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return templateHash;
|
return templateHash;
|
||||||
|
@ -15,10 +15,21 @@ module.exports = class Logger {
|
|||||||
const options = {
|
const options = {
|
||||||
base: null, // Do not bind hostname, pid and friends by default
|
base: null, // Do not bind hostname, pid and friends by default
|
||||||
level: LOG_LEVEL || logLevelFromNodeEnv,
|
level: LOG_LEVEL || logLevelFromNodeEnv,
|
||||||
|
formatters: {
|
||||||
|
level (label) {
|
||||||
|
if (label === 'warn') {
|
||||||
|
return { levelname: 'warning' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { levelname: label };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messageKey: 'message',
|
||||||
|
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||||
serializers: {
|
serializers: {
|
||||||
request: requestSerializer,
|
client_request: requestSerializer,
|
||||||
response: responseSerializer,
|
server_response: responseSerializer,
|
||||||
error: (error) => Array.isArray(error) ? error.map((err) => errorSerializer(err)) : [errorSerializer(error)]
|
exception: errorSerializer
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const dest = pino.destination({ sync: false }); // stdout
|
const dest = pino.destination({ sync: false }); // stdout
|
||||||
|
94
metro/config.json
Normal file
94
metro/config.json
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"metrics": {
|
||||||
|
"port": 9145,
|
||||||
|
"definitions": [
|
||||||
|
{
|
||||||
|
"type": "counter",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_requests_total",
|
||||||
|
"help": "MAPS API requests total"
|
||||||
|
},
|
||||||
|
"valuePath": "server_response.statusCode",
|
||||||
|
"shouldMeasure": "({ value }) => Number.isFinite(value)",
|
||||||
|
"measure": "({ metric }) => metric.inc()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "counter",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_requests_ok_total",
|
||||||
|
"help": "MAPS API requests ok total"
|
||||||
|
},
|
||||||
|
"valuePath": "server_response.statusCode",
|
||||||
|
"shouldMeasure": "({ value }) => value >= 200 && value < 400",
|
||||||
|
"measure": "({ metric }) => metric.inc()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "counter",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_requests_errors_total",
|
||||||
|
"help": "MAPS API requests errors total"
|
||||||
|
},
|
||||||
|
"valuePath": "server_response.statusCode",
|
||||||
|
"shouldMeasure": "({ value }) => value >= 400",
|
||||||
|
"measure": "({ metric }) => metric.inc()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "histogram",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_response_time_total",
|
||||||
|
"help": "MAPS API response time total"
|
||||||
|
},
|
||||||
|
"valuePath": "stats.response",
|
||||||
|
"shouldMeasure": "({ value }) => Number.isFinite(value)",
|
||||||
|
"measure": "({ metric, value }) => metric.observe(value)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "counter",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_requests",
|
||||||
|
"help": "MAPS API requests per user",
|
||||||
|
"labelNames": ["user", "http_code"]
|
||||||
|
},
|
||||||
|
"labelPaths": ["cdb-user", "server_response.statusCode"],
|
||||||
|
"shouldMeasure": "({ labels }) => labels.every((label) => label !== undefined)",
|
||||||
|
"measure": "({ metric, labels }) => metric.labels(...labels).inc()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "counter",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_requests_ok",
|
||||||
|
"help": "MAPS API requests per user with success HTTP code",
|
||||||
|
"labelNames": ["user", "http_code"]
|
||||||
|
},
|
||||||
|
"labelPaths": ["cdb-user", "server_response.statusCode"],
|
||||||
|
"valuePath": "server_response.statusCode",
|
||||||
|
"shouldMeasure": "({ labels, value }) => labels.every((label) => label !== undefined) && value >= 200 && value < 400",
|
||||||
|
"measure": "({ metric, labels }) => metric.labels(...labels).inc()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "counter",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_requests_errors",
|
||||||
|
"help": "MAPS API requests per user with error HTTP code",
|
||||||
|
"labelNames": ["user", "http_code"]
|
||||||
|
},
|
||||||
|
"labelPaths": ["cdb-user", "server_response.statusCode"],
|
||||||
|
"valuePath": "server_response.statusCode",
|
||||||
|
"shouldMeasure": "({ labels, value }) => labels.every((label) => label !== undefined) && value >= 400",
|
||||||
|
"measure": "({ metric, labels }) => metric.labels(...labels).inc()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "histogram",
|
||||||
|
"options": {
|
||||||
|
"name": "maps_api_response_time",
|
||||||
|
"help": "MAPS API response time total",
|
||||||
|
"labelNames": ["user"]
|
||||||
|
},
|
||||||
|
"labelPaths": ["cdb-user"],
|
||||||
|
"valuePath": "stats.response",
|
||||||
|
"shouldMeasure": "({ labels, value }) => labels.every((label) => label !== undefined) && Number.isFinite(value)",
|
||||||
|
"measure": "({ metric, labels, value }) => metric.labels(...labels).observe(value)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,40 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const split = require('split2');
|
const metro = require('./metro');
|
||||||
const logCollector = require('./log-collector');
|
const path = require('path');
|
||||||
const metricsCollector = require('./metrics-collector');
|
const fs = require('fs');
|
||||||
|
|
||||||
const streams = [process.stdin, split(), logCollector(), metricsCollector(), process.stdout];
|
const { CONFIG_PATH = path.resolve(__dirname, './config.json') } = process.env;
|
||||||
|
const existsConfigFile = fs.existsSync(CONFIG_PATH);
|
||||||
|
|
||||||
pipeline('pipe', streams);
|
if (!existsConfigFile) {
|
||||||
|
exit(4)(new Error(`Wrong path for CONFIG_PATH env variable: ${CONFIG_PATH} no such file`));
|
||||||
|
}
|
||||||
|
|
||||||
process.on('SIGINT', exitProcess(0));
|
let config;
|
||||||
process.on('SIGTERM', exitProcess(0));
|
|
||||||
process.on('uncaughtException', exitProcess(1));
|
|
||||||
process.on('unhandledRejection', exitProcess(1));
|
|
||||||
|
|
||||||
function pipeline (action, streams) {
|
if (existsConfigFile) {
|
||||||
for (let index = 0; index < streams.length - 1; index++) {
|
config = fs.readFileSync(CONFIG_PATH);
|
||||||
const source = streams[index];
|
try {
|
||||||
const destination = streams[index + 1];
|
config = JSON.parse(config);
|
||||||
source[action](destination);
|
} catch (e) {
|
||||||
|
exit(5)(new Error('Wrong config format: invalid JSON'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitProcess (code = 0) {
|
metro({ metrics: config && config.metrics })
|
||||||
return function exitProcess (signal) {
|
.then(exit(0))
|
||||||
pipeline('unpipe', streams);
|
.catch(exit(1));
|
||||||
|
|
||||||
|
process.on('uncaughtException', exit(2));
|
||||||
|
process.on('unhandledRejection', exit(3));
|
||||||
|
|
||||||
|
function exit (code = 1) {
|
||||||
|
return function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const split = require('split2');
|
|
||||||
const assingDeep = require('assign-deep');
|
|
||||||
const { Transform } = require('stream');
|
|
||||||
const DEV_ENVS = ['test', 'development'];
|
|
||||||
const dumpPath = `${__dirname}/dump.json`;
|
|
||||||
|
|
||||||
let logs;
|
|
||||||
|
|
||||||
const LEVELS = {
|
|
||||||
10: 'trace',
|
|
||||||
20: 'debug',
|
|
||||||
30: 'info',
|
|
||||||
40: 'warn',
|
|
||||||
50: 'error',
|
|
||||||
60: 'fatal'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function logCollector () {
|
|
||||||
const stream = new Transform({
|
|
||||||
transform (chunk, enc, callback) {
|
|
||||||
let entry;
|
|
||||||
|
|
||||||
try {
|
|
||||||
entry = JSON.parse(chunk);
|
|
||||||
const { level, time } = entry;
|
|
||||||
|
|
||||||
if (level === undefined && time === undefined) {
|
|
||||||
throw new Error('Entry log is not valid');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (DEV_ENVS.includes(process.env.NODE_ENV)) {
|
|
||||||
this.push(chunk + '\n');
|
|
||||||
}
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = entry;
|
|
||||||
|
|
||||||
if (id === undefined) {
|
|
||||||
entry.level = LEVELS[entry.level];
|
|
||||||
this.push(`${JSON.stringify(entry)}\n`);
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logs.has(id)) {
|
|
||||||
const accEntry = logs.get(id);
|
|
||||||
const { end } = entry;
|
|
||||||
|
|
||||||
if (end === true) {
|
|
||||||
accEntry.level = LEVELS[accEntry.level];
|
|
||||||
accEntry.time = entry.time;
|
|
||||||
this.push(`${JSON.stringify(accEntry)}\n`);
|
|
||||||
logs.delete(id);
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accEntry.level > entry.level) {
|
|
||||||
delete entry.level;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasProperty(accEntry, 'error') && hasProperty(entry, 'error')) {
|
|
||||||
logs.set(id, assingDeep({}, accEntry, entry, { error: accEntry.error.concat(entry.error) }));
|
|
||||||
} else {
|
|
||||||
logs.set(id, assingDeep({}, accEntry, entry));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logs.set(id, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('pipe', () => {
|
|
||||||
if (!fs.existsSync(dumpPath)) {
|
|
||||||
logs = new Map();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dump = require(dumpPath);
|
|
||||||
logs = new Map(dump);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Cannot read the dump for unfinished logs: ${err}`);
|
|
||||||
logs = new Map();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('unpipe', () => {
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(dumpPath, JSON.stringify([...logs]));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Cannot create a dump for unfinished logs: ${err}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
function hasProperty (obj, prop) {
|
|
||||||
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
||||||
}
|
|
@ -1,121 +1,102 @@
|
|||||||
'use strict'
|
'use strict';
|
||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const { Counter, Histogram, register } = require('prom-client');
|
const { Counter, Histogram, register } = require('prom-client');
|
||||||
const split = require('split2');
|
const flatten = require('flat');
|
||||||
const { Transform } = require('stream');
|
const { Transform, PassThrough } = require('stream');
|
||||||
const DEV_ENVS = ['test', 'development'];
|
const DEV_ENVS = ['test', 'development'];
|
||||||
|
|
||||||
const requestCounter = new Counter({
|
const factory = {
|
||||||
name: 'maps_api_requests_total',
|
counter: Counter,
|
||||||
help: 'MAPS API requests total'
|
histogram: Histogram
|
||||||
});
|
};
|
||||||
|
|
||||||
const requestOkCounter = new Counter({
|
module.exports = class MetricsCollector {
|
||||||
name: 'maps_api_requests_ok_total',
|
constructor ({ port = 0, definitions } = {}) {
|
||||||
help: 'MAPS API requests ok total'
|
this._port = port;
|
||||||
});
|
this._definitions = definitions;
|
||||||
|
this._server = null;
|
||||||
|
this._stream = createTransformStream(this._definitions);
|
||||||
|
}
|
||||||
|
|
||||||
const requestErrorCounter = new Counter({
|
get stream () {
|
||||||
name: 'maps_api_requests_errors_total',
|
return this._stream;
|
||||||
help: 'MAPS API requests errors total'
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const responseTimeHistogram = new Histogram({
|
start () {
|
||||||
name: 'maps_api_response_time_total',
|
return new Promise((resolve, reject) => {
|
||||||
help: 'MAPS API response time total'
|
this._server = http.createServer((req, res) => {
|
||||||
});
|
res.writeHead(200, { 'Content-Type': register.contentType });
|
||||||
|
res.end(register.metrics());
|
||||||
|
});
|
||||||
|
|
||||||
const userRequestCounter = new Counter({
|
this._server.once('error', err => reject(err));
|
||||||
name: 'maps_api_requests',
|
this._server.once('listening', () => resolve());
|
||||||
help: 'MAPS API requests per user',
|
this._server.listen(this._port);
|
||||||
labelNames: ['user', 'http_code']
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const userRequestOkCounter = new Counter({
|
stop () {
|
||||||
name: 'maps_api_requests_ok',
|
return new Promise((resolve) => {
|
||||||
help: 'MAPS API requests per user with success HTTP code',
|
register.clear();
|
||||||
labelNames: ['user', 'http_code']
|
if (!this._server) {
|
||||||
});
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
const userRequestErrorCounter = new Counter({
|
this._server.once('close', () => {
|
||||||
name: 'maps_api_requests_errors',
|
this._server = null;
|
||||||
help: 'MAPS API requests per user with error HTTP code',
|
resolve();
|
||||||
labelNames: ['user', 'http_code']
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const userResponseTimeHistogram = new Histogram({
|
this._server.close();
|
||||||
name: 'maps_api_response_time',
|
});
|
||||||
help: 'MAPS API response time total',
|
};
|
||||||
labelNames: ['user']
|
};
|
||||||
});
|
|
||||||
|
function createTransformStream (definitions) {
|
||||||
|
if (typeof definitions !== 'object') {
|
||||||
|
return new PassThrough();
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = [];
|
||||||
|
|
||||||
|
for (const { type, options, valuePath, labelPaths, shouldMeasure, measure } of definitions) {
|
||||||
|
metrics.push({
|
||||||
|
instance: new factory[type](options),
|
||||||
|
valuePath,
|
||||||
|
labelPaths,
|
||||||
|
shouldMeasure: eval(shouldMeasure), // eslint-disable-line no-eval
|
||||||
|
measure: eval(measure) // eslint-disable-line no-eval
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function metricsCollector () {
|
|
||||||
return new Transform({
|
return new Transform({
|
||||||
transform (chunk, enc, callback) {
|
transform (chunk, enc, callback) {
|
||||||
let entry;
|
let entry;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
entry = JSON.parse(chunk);
|
entry = JSON.parse(chunk);
|
||||||
const { level, time } = entry;
|
|
||||||
|
|
||||||
if (level === undefined && time === undefined) {
|
|
||||||
throw new Error('Entry log is not valid');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (DEV_ENVS.includes(process.env.NODE_ENV)) {
|
if (DEV_ENVS.includes(process.env.NODE_ENV)) {
|
||||||
this.push(chunk);
|
this.push(chunk + '\n');
|
||||||
}
|
}
|
||||||
return callback();
|
return callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { request, response, stats } = entry;
|
const flatEntry = flatten(entry);
|
||||||
|
|
||||||
if (request === undefined || response === undefined || stats === undefined) {
|
for (const metric of metrics) {
|
||||||
this.push(chunk);
|
const value = flatEntry[metric.valuePath];
|
||||||
return callback();
|
const labels = Array.isArray(metric.labelPaths) && metric.labelPaths.map(path => flatEntry[path]);
|
||||||
}
|
|
||||||
|
|
||||||
const { statusCode, headers } = response;
|
if (metric.shouldMeasure({ labels, value })) {
|
||||||
const { 'carto-user': user } = headers;
|
metric.measure({ metric: metric.instance, labels, value });
|
||||||
|
|
||||||
requestCounter.inc();
|
|
||||||
|
|
||||||
if (statusCode !== undefined && user !== undefined) {
|
|
||||||
userRequestCounter.labels(user, `${statusCode}`).inc();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode >= 200 && statusCode < 400) {
|
|
||||||
requestOkCounter.inc();
|
|
||||||
if (user !== undefined) {
|
|
||||||
userRequestOkCounter.labels(user, `${statusCode}`).inc();
|
|
||||||
}
|
|
||||||
} else if (statusCode >= 400) {
|
|
||||||
requestErrorCounter.inc();
|
|
||||||
if (user !== undefined) {
|
|
||||||
userRequestErrorCounter.labels(user, `${statusCode}`).inc();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { response: responseTime } = stats;
|
this.push(`${JSON.stringify(entry)}\n`);
|
||||||
|
|
||||||
if (Number.isFinite(responseTime)) {
|
return callback();
|
||||||
responseTimeHistogram.observe(responseTime);
|
|
||||||
userResponseTimeHistogram.labels(user).observe(responseTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.push(chunk);
|
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = process.env.PORT || 9145;
|
|
||||||
|
|
||||||
http
|
|
||||||
.createServer((req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': register.contentType });
|
|
||||||
res.end(register.metrics());
|
|
||||||
})
|
|
||||||
.listen(port)
|
|
||||||
.unref();
|
|
||||||
|
19
metro/metro.js
Normal file
19
metro/metro.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const util = require('util');
|
||||||
|
const stream = require('stream');
|
||||||
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
const split = require('split2');
|
||||||
|
const MetricsCollector = require('./metrics-collector');
|
||||||
|
|
||||||
|
module.exports = async function metro ({ input = process.stdin, output = process.stdout, metrics = {} } = {}) {
|
||||||
|
const metricsCollector = new MetricsCollector(metrics);
|
||||||
|
const { stream: metricsStream } = metricsCollector;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await metricsCollector.start();
|
||||||
|
await pipeline(input, split(), metricsStream, output);
|
||||||
|
} finally {
|
||||||
|
await metricsCollector.stop();
|
||||||
|
}
|
||||||
|
};
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -814,9 +814,9 @@
|
|||||||
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo="
|
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo="
|
||||||
},
|
},
|
||||||
"camshaft": {
|
"camshaft": {
|
||||||
"version": "0.66.0",
|
"version": "0.67.0",
|
||||||
"resolved": "https://registry.npmjs.org/camshaft/-/camshaft-0.66.0.tgz",
|
"resolved": "https://registry.npmjs.org/camshaft/-/camshaft-0.67.0.tgz",
|
||||||
"integrity": "sha512-25WFuUEiPD9Tw0XWSno/m7qJ7QqaEy9t/WbcXt+j+zMTOxvQ4RqpvCAhmUEbLgTLRyTCTvY8nqPtu4PaVJuIyw==",
|
"integrity": "sha512-IiE4y01qZmIJUm88Zy8bi6oNo2Y1lG4cYL/WPiqVT/HOqk5c2FBDpcOZU1LSK8M1KJmNX8zIaoUWVhs40w3HZQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "^1.5.2",
|
"async": "^1.5.2",
|
||||||
"cartodb-psql": "0.14.0",
|
"cartodb-psql": "0.14.0",
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"assign-deep": "^1.0.1",
|
"assign-deep": "^1.0.1",
|
||||||
"basic-auth": "2.0.0",
|
"basic-auth": "2.0.0",
|
||||||
"body-parser": "1.18.3",
|
"body-parser": "1.18.3",
|
||||||
"camshaft": "^0.66.0",
|
"camshaft": "^0.67.0",
|
||||||
"cartodb-psql": "0.14.0",
|
"cartodb-psql": "0.14.0",
|
||||||
"cartodb-query-tables": "^0.7.0",
|
"cartodb-query-tables": "^0.7.0",
|
||||||
"cartodb-redis": "^3.0.0",
|
"cartodb-redis": "^3.0.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user