UNPKG

@workablehr/riviere

Version:

log inbound/outbound HTTP traffic

298 lines (253 loc) 8.37 kB
const logTags = require('../logTags'); const pick = require('lodash/pick'); const pickBy = require('lodash/pickBy'); const mapValues = require('lodash/mapValues'); const isObject = require('lodash/isObject'); const url = require('url'); const isHealth = require('./isHealth'); const { extractHeaders, truncateText } = require('../utils'); const mapValuesDeep = (v, callback) => { if (v && Array.isArray(v)) { return v.map(vv => mapValuesDeep(vv, callback)); } else if (v instanceof Date) { return v; } else if (isObject(v)) { return mapValues(v, vv => mapValuesDeep(vv, callback)); } else { return callback(v); } }; const truncateTextWithLength = length => v => (typeof v === 'string' && truncateText(v, length, '[Trimmed by riviere]')) || v; const mapError = ({ ctx, err }) => { err.action = `${ctx.logCtx.method}${ctx.request.path}`; err.params = { query: ctx.request.query, body: ctx.request.body, log_tag: logTags.CATEGORY.UNEXPECTED_ERROR.TAG }; err.context = Object.assign(err.context || {}, ctx.logCtx); return err; }; /** * Server HTTP in and out responses */ const mapInReq = ({ ctx, health, bodyKeys, bodyKeysRegex, bodyKeysCallback, headersRegex, headerValueCallback, maxBodyValueChars }) => { if (isHealth(ctx, health)) { return Object.assign({}, ctx.logCtx, { log_tag: logTags.CATEGORY.INBOUND_REQUEST_HEALTH.TAG }); } const meta = extractRequestMeta( ctx, bodyKeys, bodyKeysRegex, bodyKeysCallback, maxBodyValueChars, headersRegex, headerValueCallback ); let userAgent = getUserAgent(ctx.headers); return Object.assign({}, ctx.logCtx, meta, { log_tag: logTags.CATEGORY.INBOUND_REQUEST.TAG, userAgent }); }; const mapOutRes = ({ ctx, health, bodyKeys, bodyKeysRegex, bodyKeysCallback, headersRegex, headerValueCallback, maxBodyValueChars }) => { const status = ctx.status; const duration = new Date().getTime() - ctx.state.riviereStartedAt; const headers = extractHeaders(headersRegex, headerValueCallback, ctx.response.headers); if (isHealth(ctx, health)) { return Object.assign({}, ctx.logCtx, { log_tag: logTags.CATEGORY.OUTBOUND_RESPONSE_HEALTH.TAG, status, duration }); } const meta = extractRequestMeta( ctx, bodyKeys, bodyKeysRegex, bodyKeysCallback, maxBodyValueChars, headersRegex, headerValueCallback, 'request' ); let contentLength = ctx.state.calculatedContentLength || 0; let userAgent = getUserAgent(ctx.headers); return Object.assign({ status, duration, headers }, ctx.logCtx, meta, { log_tag: logTags.CATEGORY.OUTBOUND_RESPONSE.TAG, contentLength, userAgent }); }; /** * Api call logs */ const mapInRes = (res, req, startedAt, reqId, opts) => { const duration = new Date().getTime() - startedAt; const status = res.statusCode; let contentLength = getContentLength(res.headers); let userAgent = getUserAgent(res.headers); const headers = extractHeaders(opts.headersRegex, opts.headerValueCallback, res.headers); const hostFieldName = opts.hostFieldName || 'host'; return { ...pick(req, ['method', 'protocol', hostFieldName, 'path', 'query', 'href']), status, duration, requestId: reqId, contentLength, userAgent, log_tag: logTags.CATEGORY.INBOUND_RESPONSE.TAG, metaHeaders: { request: req.metaHeaders, ...headers } }; }; const mapOutReq = (requestOptions, reqId, opts = {}) => { const method = requestOptions.method; const port = requestOptions.port; const requestId = reqId; const headersRegex = opts.headersRegex; const headerValueCallback = opts.headerValueCallback; const hostFieldName = opts.hostFieldName || 'host'; const { protocol, host, path, query, href } = getUrlParameters(requestOptions); const { maxQueryChars, maxPathChars, maxHrefChars } = opts; const slicedQuery = truncateText(query, maxQueryChars); const slicedPath = truncateText(path, maxPathChars); const slicedHref = truncateText(href, maxHrefChars); const metaHeaders = extractHeaders(headersRegex, headerValueCallback, requestOptions.headers); let metaBody = {}; const hasJsonContentType = requestOptions.headers && ((requestOptions.headers['Content-type'] && requestOptions.headers['Content-type'].includes('application/json')) || (requestOptions.headers['content-type'] && requestOptions.headers['content-type'].includes('application/json')) || (requestOptions.headers['Content-Type'] && requestOptions.headers['Content-Type'].includes('application/json'))); const isProperMethod = method === 'POST' || method === 'PUT' || method === 'PATCH'; const hasBodyKeysSelector = opts.bodyKeys || opts.bodyKeysRegex || (opts.bodyKeysCallback && typeof opts.bodyKeysCallback === 'function'); const isValidToExtractBody = isProperMethod && requestOptions.body && hasJsonContentType && hasBodyKeysSelector; if (isValidToExtractBody) { try { const jsonObject = JSON.parse(requestOptions.body); let picked = opts.maxBodyValueChars ? mapValuesDeep(jsonObject, truncateTextWithLength(opts.maxBodyValueChars)) : jsonObject; if (opts.bodyKeysCallback && typeof opts.bodyKeysCallback === 'function') { picked = opts.bodyKeysCallback(picked); } else if (opts.bodyKeysRegex) { const REGEX = opts.bodyKeysRegex; picked = pickBy(picked, (_, key) => REGEX.test(key)); } else { picked = pick(picked, opts.bodyKeys); } if (Object.keys(picked).length) { metaBody = { body: picked }; } } catch (e) {} } let contentLength = getContentLength(requestOptions.headers); const returnObject = { metaHeaders, method, protocol, port, metaBody, path: slicedPath, query: slicedQuery, href: slicedHref, requestId, contentLength, log_tag: logTags.CATEGORY.OUTBOUND_REQUEST.TAG }; returnObject[hostFieldName] = host; return returnObject; }; function extractRequestMeta( ctx, bodyKeys, bodyKeysRegex, bodyKeysCallback, maxBodyValueChars, headersRegex, headerValueCallback, prefix = '' ) { const method = ctx.request.method; // pick headers const metaHeaders = extractHeaders(headersRegex, headerValueCallback, ctx.request.headers, prefix); // pick body let metaBody; const isProperMethod = method === 'POST' || method === 'PUT' || method === 'PATCH'; const hasBodyKeysSelector = bodyKeys || bodyKeysRegex || (bodyKeysCallback && typeof bodyKeysCallback === 'function'); if (isProperMethod && hasBodyKeysSelector && typeof ctx.request.body === 'object') { let picked = maxBodyValueChars ? mapValuesDeep(ctx.request.body, truncateTextWithLength(maxBodyValueChars)) : ctx.request.body; if (bodyKeysCallback && typeof bodyKeysCallback === 'function') { picked = bodyKeysCallback(picked, ctx); } else if (bodyKeysRegex) { const REGEX = bodyKeysRegex; picked = pickBy(picked, (_, key) => REGEX.test(key)); } else { picked = pick(picked, bodyKeys); } if (Object.keys(picked).length) { if (prefix) { metaBody = { [prefix]: { body: picked } }; } else { metaBody = { body: picked }; } } } const meta = {}; if (metaHeaders && Object.keys(metaHeaders).length) { meta.metaHeaders = metaHeaders; } if (metaBody) { meta.metaBody = metaBody; } return meta; } const getContentLength = headers => { if (!headers) return 0; return headers['content-length'] || headers['Content-length'] || headers['Content-Length'] || 0; }; const getUserAgent = headers => { if (!headers) return ''; return headers['user-agent'] || headers['User-agent'] || headers['User-Agent'] || ''; }; const getUrlParameters = requestOptions => { const usedOptions = requestOptions.uri || requestOptions; let protocol = usedOptions.protocol; protocol = protocol && protocol.substring(0, protocol.length - 1); const host = usedOptions.hostname || usedOptions.host; const path = usedOptions.path || usedOptions.pathname; const query = usedOptions.query; const href = url.format(usedOptions); return { protocol, host, path, query, href }; }; module.exports = { mapError, mapInReq, mapInRes, mapOutReq, mapOutRes };