UNPKG

moesif-nodejs

Version:

Monitoring agent to log API calls to Moesif for deep API analytics

318 lines (282 loc) 9.76 kB
'use strict'; var http = require('http'); var https = require('https'); var dataUtils = require('./dataUtils'); var util = require('util'); var nodeUrl = require('url'); var getEventModelFromRequestAndResponse = dataUtils.getEventModelFromRequestAndResponse; var appendChunk = dataUtils.appendChunk; function isMoesif(request, requestOptions) { if (typeof requestOptions === 'string') { if(requestOptions.includes('moesif.net')) return true; } if (request && typeof request.getHeader === 'function') { if (request.getHeader('X-Moesif-SDK') || request.getHeader('X-Moesif-Application-Id')) return true; } if (requestOptions && requestOptions.host && typeof requestOptions.host === 'string') { if (requestOptions.host.includes('moesif.net')) return true; } if (requestOptions && requestOptions.headers) { if (requestOptions.headers['X-Moesif-SDK'] || requestOptions.headers['X-Moesif-Application-Id']) return true; } return false; } // based on https://github.com/nodejs/node/blob/0324529e0fa234b8102c1a6a1cde19c76a6fff82/lib/internal/url.js#L1406 function urlToHttpOptions(url) { const options = { protocol: url.protocol, hostname: typeof url.hostname === 'string' && url.hostname.indexOf('[') === 0 ? url.hostname.slice(1, -1) : url.hostname, hash: url.hash, search: url.search, pathname: url.pathname, path: `${url.pathname || ""}${url.search || ""}`, href: url.href }; if (url.port !== '') { options.port = Number(url.port); } if (url.username || url.password) { options.auth = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`; } return options; } // handle these scenarios // http.request(options) // http.request(urlString, options); // http.request(urlString); (simple get). // http.request(URLObject, options); // http.request(URLObject); (simple get). // below is based on official nodejs code for http function standardizeRequestOption(input, options, cb) { if (typeof input === 'string') { const urlStr = input; input = urlToHttpOptions(new nodeUrl.URL(urlStr)); } else if (input instanceof nodeUrl.URL) { // url.URL instance input = urlToHttpOptions(input); } else { cb = options; options = input; input = null; } if (typeof options === 'function') { cb = options; options = input || {}; } else { options = Object.assign(input || {}, options); } return options; } function track(requestOptions, request, recorder, logger, moesifOptions) { if (isMoesif(request, requestOptions)) { logger('skip capturing requests to moesif itself'); return; } var startTime = new Date(); var originalRequestWrite = request.write; var requestBody = null; var finished = false; var debugString = requestOptions; if (typeof requestOptions === 'object' && requestOptions); { debugString = (requestOptions.hostname || requestOptions.host) + (requestOptions.path || requestOptions.pathname); logger('initiating capturing of outing ' + util.inspect(requestOptions)); } request.write = function(chunk, encoding, callback) { var writeReturnValue = originalRequestWrite.call(request, chunk, encoding, callback); logger("write outgoing request body for " + debugString + chunk); requestBody = appendChunk(requestBody, chunk); return writeReturnValue; }; var originalRequestEnd = request.end; request.end = function(chunk, encoding, callback) { var endReturnValue = originalRequestEnd.call(request, chunk, encoding, callback); logger('end outgoing request body for ' + debugString + chunk); requestBody = appendChunk(requestBody, chunk); return endReturnValue; }; request.on("response", function (res) { var responseBody = null; logger("on response triggered in moesif " + debugString); var endTime = new Date(); // this will most likely be overriden. if (moesifOptions && moesifOptions.outgoingPatch) { var myStream = res; var dataEventTracked = false; var endEventTracked = false; myStream._mo_on = myStream.on; myStream.on = function (evt, handler) { var passOnHandler = handler; if (evt === "data" && !dataEventTracked) { logger("tracking outgoing response Data Event " + debugString); dataEventTracked = true; passOnHandler = function (chs) { logger( "outgoing response Data handler received for " + debugString + " " + chs ); responseBody = appendChunk(responseBody, chs); // always update end time in case end event is not triggered. endTime = new Date(); return handler(chs); }; } else if (evt === "end" && !endEventTracked) { logger("tracking outgoing response End event " + debugString); endEventTracked = true; passOnHandler = function (chs) { logger("outgoing response End handler" + debugString); endTime = new Date(); if (!finished) { finished = true; recorder( getEventModelFromRequestAndResponse( requestOptions, request, startTime, requestBody, res, endTime, responseBody ) ); } return handler(chs); }; } return myStream._mo_on(evt, passOnHandler); }; } else { res.on('data', function(d) { logger('outgoing data received', d); responseBody = appendChunk(responseBody, d); }); // only triggered when an event is aborted, // at this point, since "error" on request // isn't started. I need to count on this abort to // let me know the end point. res.on('abort', function() { logger('on abort is triggered in response'); logger('raw responsebody from out going API call is'); logger(responseBody); finished = true; recorder( getEventModelFromRequestAndResponse( requestOptions, request, startTime, requestBody, res, endTime, responseBody ) ); }); res.on('end', function() { var endTime = new Date(); logger('outgoing response end event for outgoing call'); logger(responseBody); finished = true; recorder( getEventModelFromRequestAndResponse( requestOptions, request, startTime, requestBody, res, endTime, responseBody ) ); }); } }); // if req.abort() is called before request connection started. // 'error' on request is always triggered at somepoint. // but if req.abort() is called have response object already exists, // then "error" on request is not triggered. request.on('error', function(error) { logger('on error for outgoing request ' + debugString, error); finished = true; var endTime = new Date(); recorder( getEventModelFromRequestAndResponse( requestOptions, request, startTime, requestBody, null, endTime, null ) ); }); // fail safe if not finished setTimeout(() => { if (!finished) { logger('outbound request longer than 2 second, timing out. log what we have.' + debugString); finished = true; var endTime = new Date(); recorder( getEventModelFromRequestAndResponse( requestOptions, request, startTime, requestBody, null, endTime, null ) ); } }, moesifOptions.maxOutgoingTimeout || 30000); } function _patch(recorder, logger, moesifOptions) { var originalGet = http.get; var originalHttpsGet = https.get; var originalRequest = http.request; var originalHttpsRequest = https.request; // On node >= v0.11.12 and < 9.0 (excluding 8.9.0) https.request just calls http.request (with additional options). // On node < 0.11.12, 8.9.0, and 9.0 > https.request is handled separately // Patch both and leave add a _mo_tracked flag to prevent double tracking. http.request = function(options, ...requestArgs) { var request = originalRequest.call(http, options, ...requestArgs); if (!request._mo_tracked) { request._mo_tracked = true; var requestOptions = standardizeRequestOption(options, ...requestArgs); track(requestOptions, request, recorder, logger, moesifOptions); } return request; }; https.request = function(options, ...requestArgs) { var request = originalHttpsRequest.call(https, options, ...requestArgs); if (!request._mo_tracked) { request._mo_tracked = true; var requestOptions = standardizeRequestOption(options, ...requestArgs); track(requestOptions, request, recorder, logger, moesifOptions); } return request; }; http.get = function(options, ...requestArgs) { var request = http.request.call(http, options, ...requestArgs); request.end(); return request; }; https.get = function(options, ...requestArgs) { var request = https.request.call(https, options, ...requestArgs); request.end(); return request; }; function _unpatch() { http.request = originalRequest; https.request = originalHttpsRequest; http.get = originalGet; https.get = originalHttpsGet; } return _unpatch; } module.exports = _patch;