2020-02-20 18:48:32 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const EVENT_VERSION = '1';
|
2020-02-26 20:24:46 +08:00
|
|
|
const MAX_LENGTH = 100;
|
2020-02-20 18:48:32 +08:00
|
|
|
|
2020-04-29 20:32:08 +08:00
|
|
|
module.exports = function metrics ({ enabled, tags, metricsBackend, logger }) {
|
2020-04-27 18:46:27 +08:00
|
|
|
if (!enabled) {
|
2020-04-29 16:40:45 +08:00
|
|
|
return function metricsDisabledMiddleware (req, res, next) {
|
2020-04-27 17:41:37 +08:00
|
|
|
next();
|
|
|
|
};
|
2020-02-20 18:48:32 +08:00
|
|
|
}
|
|
|
|
|
2020-04-29 01:17:00 +08:00
|
|
|
if (!tags || !tags.event) {
|
|
|
|
throw new Error('Missing required "event" parameter to report metrics');
|
|
|
|
}
|
|
|
|
|
2020-04-29 16:40:45 +08:00
|
|
|
return function metricsMiddleware (req, res, next) {
|
2020-04-27 17:41:37 +08:00
|
|
|
res.on('finish', () => {
|
2020-04-29 01:17:00 +08:00
|
|
|
const { event, attributes } = getEventData(req, res, tags);
|
2020-02-20 18:48:32 +08:00
|
|
|
|
2020-04-29 01:17:00 +08:00
|
|
|
metricsBackend.send(event, attributes)
|
2020-04-29 16:40:45 +08:00
|
|
|
.then(() => logger.debug(`Event "${event}" published succesfully`))
|
|
|
|
.catch((error) => logger.error(`Failed to publish event "${event}": ${error.message}`));
|
2020-04-27 17:41:37 +08:00
|
|
|
});
|
2020-02-20 18:48:32 +08:00
|
|
|
|
|
|
|
return next();
|
|
|
|
};
|
2020-04-29 01:17:00 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
function getEventData (req, res, tags) {
|
|
|
|
const event = tags.event;
|
|
|
|
const extra = {};
|
|
|
|
if (tags.from) {
|
|
|
|
if (tags.from.req) {
|
|
|
|
Object.assign(extra, getFromReq(req, tags.from.req));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tags.from.res) {
|
|
|
|
Object.assign(extra, getFromRes(res, tags.from.res));
|
|
|
|
}
|
2020-02-20 18:48:32 +08:00
|
|
|
}
|
|
|
|
|
2020-04-29 01:17:00 +08:00
|
|
|
const attributes = Object.assign({}, {
|
|
|
|
metrics_event: normalizedField(req.get('Carto-Event')),
|
|
|
|
event_source: normalizedField(req.get('Carto-Event-Source')),
|
|
|
|
event_group_id: normalizedField(req.get('Carto-Event-Group-Id')),
|
|
|
|
event_time: new Date().toISOString(),
|
2020-02-20 18:48:32 +08:00
|
|
|
user_id: res.locals.userId,
|
2020-04-29 01:17:00 +08:00
|
|
|
user_agent: req.get('User-Agent'),
|
2020-04-29 20:32:08 +08:00
|
|
|
map_id: getLayergroupid({ res }),
|
|
|
|
cache_buster: getCacheBuster({ res }),
|
2020-02-20 18:48:32 +08:00
|
|
|
response_code: res.statusCode.toString(),
|
2020-04-29 01:17:00 +08:00
|
|
|
response_time: getResponseTime(res),
|
2020-02-20 18:48:32 +08:00
|
|
|
source_domain: req.hostname,
|
|
|
|
event_version: EVENT_VERSION
|
2020-04-29 01:17:00 +08:00
|
|
|
}, tags.attributes, extra);
|
2020-02-20 18:48:32 +08:00
|
|
|
|
2020-04-29 01:17:00 +08:00
|
|
|
// remove undefined properties
|
|
|
|
Object.keys(attributes).forEach(key => attributes[key] === undefined && delete attributes[key]);
|
2020-03-10 18:40:01 +08:00
|
|
|
|
2020-02-20 18:48:32 +08:00
|
|
|
return { event, attributes };
|
|
|
|
}
|
|
|
|
|
2020-02-26 20:24:46 +08:00
|
|
|
function normalizedField (field) {
|
2020-02-27 00:41:41 +08:00
|
|
|
if (!field) {
|
|
|
|
return undefined;
|
|
|
|
}
|
2020-02-27 00:44:53 +08:00
|
|
|
|
2020-02-26 21:50:41 +08:00
|
|
|
return field.toString().trim().substr(0, MAX_LENGTH);
|
2020-02-26 20:24:46 +08:00
|
|
|
}
|
|
|
|
|
2020-04-29 20:32:08 +08:00
|
|
|
function getLayergroupid ({ res }) {
|
|
|
|
if (res.locals.token) {
|
|
|
|
return res.locals.token;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.locals.mapConfig) {
|
|
|
|
return res.locals.mapConfig.id();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.locals.mapConfigProvider && res.locals.mapConfigProvider.mapConfig) {
|
|
|
|
return res.locals.mapConfigProvider.mapConfig.id();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCacheBuster ({ res }) {
|
|
|
|
if (res.locals.cache_buster !== undefined) {
|
|
|
|
return `${res.locals.cache_buster}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (res.locals.mapConfigProvider) {
|
|
|
|
return `${res.locals.mapConfigProvider.getCacheBuster()}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-27 19:35:19 +08:00
|
|
|
// FIXME: 'X-Tiler-Profiler' might not be accurate enough
|
2020-03-10 18:40:01 +08:00
|
|
|
function getResponseTime (res) {
|
|
|
|
const profiler = res.get('X-Tiler-Profiler');
|
|
|
|
let stats;
|
|
|
|
|
|
|
|
try {
|
|
|
|
stats = JSON.parse(profiler);
|
|
|
|
} catch (e) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2020-04-29 16:28:10 +08:00
|
|
|
return stats && stats.total ? stats.total.toString() : undefined;
|
2020-03-10 18:40:01 +08:00
|
|
|
}
|
|
|
|
|
2020-04-29 01:17:00 +08:00
|
|
|
function getFromReq (req, { query = {}, body = {}, params = {}, headers = {} } = {}) {
|
|
|
|
const extra = {};
|
|
|
|
|
|
|
|
for (const [queryParam, eventName] of Object.entries(query)) {
|
|
|
|
extra[eventName] = req.query[queryParam];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [bodyParam, eventName] of Object.entries(body)) {
|
|
|
|
extra[eventName] = req.body[bodyParam];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [pathParam, eventName] of Object.entries(params)) {
|
|
|
|
extra[eventName] = req.params[pathParam];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [header, eventName] of Object.entries(headers)) {
|
|
|
|
extra[eventName] = req.get(header);
|
|
|
|
}
|
|
|
|
|
|
|
|
return extra;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getFromRes (res, { body = {}, headers = {}, locals = {} } = {}) {
|
|
|
|
const extra = {};
|
|
|
|
|
|
|
|
if (res.body) {
|
|
|
|
for (const [bodyParam, eventName] of Object.entries(body)) {
|
|
|
|
extra[eventName] = res.body[bodyParam];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [header, eventName] of Object.entries(headers)) {
|
|
|
|
extra[eventName] = res.get(header);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [localParam, eventName] of Object.entries(locals)) {
|
|
|
|
extra[eventName] = res.locals[localParam];
|
|
|
|
}
|
|
|
|
|
|
|
|
return extra;
|
|
|
|
}
|