182 lines
5.0 KiB
JavaScript
182 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
const EVENT_VERSION = '1';
|
|
const MAX_LENGTH = 100;
|
|
|
|
module.exports = function metrics ({ enabled, tags, metricsBackend }) {
|
|
if (!enabled) {
|
|
return function metricsDisabledMiddleware (req, res, next) {
|
|
next();
|
|
};
|
|
}
|
|
|
|
if (!tags || !tags.event) {
|
|
throw new Error('Missing required "event" parameter to report metrics');
|
|
}
|
|
|
|
return function metricsMiddleware (req, res, next) {
|
|
// FIXME: use parent logger as we don't want bind the error to the request
|
|
// but we still want to know if an error is thrown
|
|
const { logger } = res.locals;
|
|
|
|
res.on('finish', () => {
|
|
const { event, attributes } = getEventData(req, res, tags);
|
|
|
|
metricsBackend.send(event, attributes)
|
|
.catch((err) => logger.error(err, `Failed to publish event "${event}"`));
|
|
});
|
|
|
|
return next();
|
|
};
|
|
};
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
const attributes = Object.assign({}, {
|
|
client_event: normalizedField(req.get('Carto-Event')),
|
|
client_event_group_id: normalizedField(req.get('Carto-Event-Group-Id')),
|
|
event_source: normalizedField(req.get('Carto-Event-Source')),
|
|
event_time: new Date().toISOString(),
|
|
user_id: res.locals.userId,
|
|
user_agent: req.get('User-Agent'),
|
|
map_id: getLayergroupid({ res }),
|
|
cache_buster: getCacheBuster({ res }),
|
|
template_hash: getTemplateHash({ res }),
|
|
stat_tag: getStatTag({ res }),
|
|
response_code: res.statusCode.toString(),
|
|
response_time: getResponseTime(req),
|
|
source_domain: req.hostname,
|
|
event_version: EVENT_VERSION
|
|
}, tags.attributes, extra);
|
|
|
|
// remove undefined properties
|
|
Object.keys(attributes).forEach(key => attributes[key] === undefined && delete attributes[key]);
|
|
|
|
return { event, attributes };
|
|
}
|
|
|
|
function normalizedField (field) {
|
|
if (!field) {
|
|
return undefined;
|
|
}
|
|
|
|
return field.toString().trim().substr(0, MAX_LENGTH);
|
|
}
|
|
|
|
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()}`;
|
|
}
|
|
}
|
|
|
|
function getTemplateHash ({ res }) {
|
|
if (res.locals.templateHash) {
|
|
return res.locals.templateHash;
|
|
}
|
|
|
|
if (res.locals.mapConfigProvider && res.locals.mapConfigProvider.getTemplateHash) {
|
|
let templateHash;
|
|
|
|
try {
|
|
templateHash = res.locals.mapConfigProvider.getTemplateHash().substring(0, 8);
|
|
} catch (e) {}
|
|
|
|
return templateHash;
|
|
}
|
|
}
|
|
|
|
function getStatTag ({ res }) {
|
|
if (res.locals.mapConfig) {
|
|
return res.locals.mapConfig.obj().stat_tag;
|
|
}
|
|
|
|
// FIXME: don't expect that mapConfig is already set
|
|
if (res.locals.mapConfigProvider && res.locals.mapConfigProvider.mapConfig) {
|
|
return res.locals.mapConfigProvider.mapConfig.obj().stat_tag;
|
|
}
|
|
}
|
|
|
|
// FIXME: 'Profiler' might not be accurate enough
|
|
function getResponseTime (req) {
|
|
let stats;
|
|
|
|
try {
|
|
stats = req.profiler.toJSON();
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
|
|
return stats && stats.total ? stats.total.toString() : undefined;
|
|
}
|
|
|
|
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;
|
|
}
|