apminsight
Version:
monitor nodejs applications
662 lines (638 loc) • 20.6 kB
JavaScript
var constants = require("./../constants");
var utils = require("./../util/utils");
var apmwrapper = require("./wrapper");
var http2Info = require("./http2");
var url = require("url");
var logger = require("./../util/logger");
var componentName = "NODEJS-CORE";
var MODULES_INFO = {
events: [
{
functionName: "prototype.addListener",
wrapper: registerEvent,
override: function (actualModule, wrappedFunc) {
actualModule.prototype.addListener = wrappedFunc;
actualModule.prototype.on = wrappedFunc;
},
component: componentName
}
],
http: [
{
functionName: "createServer",
wrapper: createServerWrap,
component: componentName
},
{
functionName: "Server",
wrapper: createServerWrap,
component: componentName
},
{
functionName: "request",
component: "HTTP",
wrapper: handleWebRequest,
extractInfo: getUrl
},
{
functionName: "get",
component: "HTTP",
wrapper: handleWebRequest,
extractInfo: getUrl
}
],
https: [
{
functionName: "createServer",
wrapper: createServerWrap,
component: componentName
},
{
functionName: "Server",
wrapper: createServerWrap,
component: componentName
},
{
functionName: "request",
component: "HTTP",
wrapper: handleWebRequest,
extractInfo: getUrl
},
{
functionName: "get",
component: "HTTP",
wrapper: handleWebRequest,
extractInfo: getUrl
}
],
http2: [
{
functionName: "createSecureServer",
wrapper: http2ServerWrap,
component: componentName
},
{
functionName: [
"Http2ServerRequest.prototype.setTimeout",
"Http2ServerResponse.prototype.end",
"Http2ServerResponse.prototype.setTimeout",
"Http2ServerResponse.prototype.createPushResponse"
],
component: componentName
}
],
net: [
{
functionName: [
"Server.prototype.getConnections",
"Server.prototype.close",
"Socket.prototype.setTimeout",
"Socket.prototype.write",
"Socket.prototype.connect",
"createConnection"
],
component: componentName
},
{
functionName: "Server.prototype.listen",
wrapper: handleListeningPort,
component: componentName
}
],
fs: [
{
functionName: [
"access",
"ftruncate",
"mkdir",
"mkdtemp",
"readdir",
"readFile",
"readlink",
"realpath",
"truncate",
"appendFile",
"symlink",
"copyFile",
"open",
"writeFile",
"chmod",
"fchmod",
"lchmod",
"link",
"rename",
"chown",
"fchown",
"lchown",
"futimes",
"utimes",
"close",
"exists",
"fdatasync",
"fstat",
"fsync",
"lstat",
"stat",
"rmdir",
"unlink",
"read",
"write",
"realpath.native"
],
component: componentName
}
],
dns: [
{
functionName: [
"lookupService",
"lookup",
"resolve",
"resolve4",
"resolve6",
"resolveCname",
"resolveMx",
"resolveNaptr",
"resolveNs",
"resolvePtr",
"resolveSoa",
"resolveSrv",
"resolveTxt",
"resolveAny",
"reverse"
],
component: componentName
}
],
crypto: [
{
functionName: [
"pbkdf2",
"randomBytes",
"randomFill",
"generateKeyPair",
"scrypt"
],
component: componentName
}
],
domain: [
{
functionName: [
"Domain.prototype.bind",
"Domain.prototype.intercept"
],
component: componentName
}
],
zlib: [
{
functionName: [
"Deflate.prototype.close",
"Inflate.prototype.close",
"Gzip.prototype.close",
"Gunzip.prototype.close",
"DeflateRaw.prototype.close",
"InflateRaw.prototype.close",
"Unzip.prototype.close",
"Deflate.prototype.flush",
"Inflate.prototype.flush",
"Gzip.prototype.flush",
"Gunzip.prototype.flush",
"DeflateRaw.prototype.flush",
"InflateRaw.prototype.flush",
"Unzip.prototype.flush",
"Deflate.prototype.params",
"deflate",
"deflateRaw",
"gunzip",
"gzip",
"inflate",
"inflateRaw",
"unzip"
],
component: componentName
}
],
child_process: [
{
functionName: ["exec", "execFile"],
component: componentName
}
],
cluster: [
{
functionName: ["disconnect", "Worker.prototype.send"],
component: componentName
}
],
process: [
{
functionName: "send",
component: componentName
},
{
functionName: "nextTick",
wrapper: nextTickWrapper,
component: componentName
}
],
dgram: [
{
functionName: [
"Socket.prototype.bind",
"Socket.prototype.close",
"Socket.prototype.send",
"createSocket"
],
component: componentName
}
],
tls: [
{
functionName: ["connect", "TLSSocket.prototype.renegotiate"],
component: componentName
}
],
stream: [
{
functionName: [
"Writable.prototype.write",
"Writable.prototype.end",
"finished",
"pipeline"
],
component: componentName
}
],
global: [
{
functionName: ["setImmediate", "setTimeout"],
callBackIndex: 0,
component: componentName
}
],
inspector: [
{
functionName: ["Session.prototype.post"],
component: componentName
}
],
worker_threads: [
{
functionName: ["Worker.prototype.terminate"],
component: componentName
}
],
readline: [
{
functionName: ["Interface.prototype.question"],
component: componentName
}
]
};
// Need to check about async_hooks modules
function createServerWrap(original, funcInfo, moduleName, compName) {
return function (option, listener) {
if (utils.isFunction(option)) {
arguments[0] = requestListener(option, moduleName, compName);
} else if (utils.isFunction(listener)) {
arguments[1] = requestListener(listener, moduleName, compName);
}
var server = original.apply(this, arguments);
wrapRequestEvent(server);
return server;
};
}
function wrapRequestEvent(server) {
if (server && utils.isFunction(server.on)) {
var actualOn = server.on;
server.on = function (event, listener) {
if (event === "request" && utils.isFunction(listener)) {
arguments[1] = requestListener(listener);
}
return actualOn.apply(this, arguments);
};
}
//Hapi framework middlewares taken from server obj
if (server && utils.isFunction(server.route)) {
var actualRoute = server.route;
server.route = function (options) {
if (options && utils.isFunction(options.handler)) {
var actualHandler = options.handler;
var trackerName = "Hapi - Middleware - Route";
arguments[0].handler = requestListener(
actualHandler,
trackerName,
"HAPI"
);
}
return actualRoute.apply(this, arguments);
};
}
if (server && utils.isFunction(server.ext)) {
var actualExt = server.ext;
server.ext = function (extPoint, handler) {
if (extPoint && handler && utils.isFunction(handler)) {
var actualHandler = handler;
var trackerName = "Hapi - Middleware - Ext - " + extPoint;
arguments[1] = requestListener(
actualHandler,
trackerName,
"HAPI"
);
}
return actualExt.apply(this, arguments);
};
}
if (server && utils.isFunction(server.decorate)) {
var actualDecor = server.decorate;
server.decorate = function (type, property, handler) {
if (type && property && handler && utils.isFunction(handler)) {
var actualHandler = handler;
var trackerName =
"Hapi - Middleware - " + type + " - " + property;
arguments[2] = requestListener(
actualHandler,
trackerName,
"HAPI"
);
}
return actualDecor.apply(this, arguments);
};
}
}
function http2ServerWrap(original) {
return function (option, listener) {
if (utils.isFunction(listener)) {
arguments[1] = requestListener(listener);
}
var server = original.apply(this, arguments);
if (server && server.on) {
server.on("request", http2Info.handleHttp2Request);
var actualOn = server.on;
server.on = function (event, handler) {
if (
(event === "stream" || event === "session") &&
utils.isFunction(handler)
) {
arguments[1] = http2Info.handleHttp2StreamOrSession(
handler,
event
);
}
return actualOn.apply(this, arguments);
};
}
return server;
};
}
function requestListener(actualListener, tName, compName) {
return function (req, res) {
if (!req) {
return actualListener.apply(this, arguments);
}
var rootTrackerName;
var trackerName;
var listenerName = utils.isEmpty(actualListener.name)
? "Anonymous"
: actualListener.name;
//Hapi framework assigns actual req object to raw, so our apminsightTxn is inside req object of raw
var curTransaction =
req.apminsightTxn ||
(req.raw && req.raw.req.apminsightTxn) ||
apmInsightAgentInstance.getCurTxn();
if (curTransaction) {
try {
if (!utils.isEmpty(tName)) {
trackerName = tName + " - " + listenerName;
} else {
trackerName = "emit - request - " + listenerName;
}
var currentTracker = apmInsightAgentInstance.getCurTracker();
var trackerInfo = { trackerName: trackerName, sync: true, parent: currentTracker };
trackerInfo.component = compName ? compName : "NODEJS-CORE";
var syncOpnInfo = {
txn: curTransaction,
trackerInfo: trackerInfo
};
return apmwrapper.invokeSyncOpn(
actualListener,
this,
arguments,
syncOpnInfo
);
} catch (e) {
logger.error("Error happend in the tracker : " + trackerName);
throw e;
} finally {
apmInsightAgentInstance.clearCurContext();
}
}
rootTrackerName = "HTTP - Request";
if (req.method && req.url) {
rootTrackerName += " - " + req.method + " - " + req.url;
}
rootTrackerName += " - " + listenerName;
var txn = apmInsightAgentInstance.createTxn(req, res, rootTrackerName);
req.apminsightTxn = txn;
if (req.stream && req.stream.session) {
req.stream.apminsightTxn = txn;
req.stream.session.apminsightTxn = txn;
}
try {
var result = actualListener.apply(this, arguments);
apmInsightAgentInstance.endRootTracker();
return result;
} catch (err) {
apmInsightAgentInstance.endRootTracker(err);
} finally {
apmInsightAgentInstance.clearCurContext();
}
};
}
function handleWebRequest(
actualCaller,
functionInfo,
moduleName,
functionName
) {
return function () {
var curTxn = apmInsightAgentInstance.getCurTxn();
if (!curTxn || curTxn.isCompleted()) {
return actualCaller.apply(this, arguments);
}
var parentTracker = apmInsightAgentInstance.getCurTracker();
var isWebRequest =
parentTracker && parentTracker.getComponent() === "HTTP";
if (isWebRequest) {
return actualCaller.apply(this, arguments);
}
var AsyncFuncInfo = {
curTxn: curTxn,
parentTracker: parentTracker,
moduleName: moduleName,
functionInfo: functionInfo,
functionName: functionName
};
checkAndInjectCrossAppRequest(arguments, curTxn);
return apmwrapper.invokeAsyncOpn(
actualCaller,
this,
arguments,
AsyncFuncInfo
);
};
}
function handleListeningPort(actual) {
return function (connectionObj) {
if (!apmInsightAgentInstance.getConfig().isPortConfiguredProperly()) {
var port;
if (utils.isPositiveNumber(connectionObj)) {
port = connectionObj;
} else if (utils.isNonEmptyString(connectionObj)) {
port = Number(connectionObj);
} else if (utils.isObject(connectionObj) && connectionObj.port) {
port = connectionObj.port;
}
apmInsightAgentInstance.getConfig().setApplicationPort(port);
logger.info("Application port number is auto-detected as :: " + port);
}
return actual.apply(this, arguments);
};
}
function checkAndInjectCrossAppRequest(args, curTxn) {
const isDistributedTracingEnabled = apmInsightAgentInstance.getConfig().isDataExporterEnabled() ||
utils.getGenericThreshold(curTxn.getUrl()).isDistributedTracingEnabled();
if (!isDistributedTracingEnabled) {
return;
}
if (args && args.length > 0 && typeof args[0] === "object") {
var reqInfo = args[0];
if (!reqInfo.headers) {
reqInfo.headers = {};
}
reqInfo.headers[constants.crossAppReqHeader] = apmInsightAgentInstance
.getConfig()
.getLicenseKeySuffix();
}
}
function validateCrossAppResponse(requestObj, tracker) {
if (requestObj && typeof requestObj.on === "function") {
requestObj.on("response", function (response) {
if (response && response.headers) {
var dtInfo = response.headers[constants.crossAppResHeaderLower];
if (utils.isNonEmptyString(dtInfo)) {
try {
dtInfo = JSON.parse(dtInfo);
tracker.updateDtInfo(dtInfo);
} catch (e) {
logger.error("Error occured in parsing dtInfo.");
}
}
}
});
}
}
function getUrl(invoker, params, returnObj, tracker, asyncOpnInfo) {
validateCrossAppResponse(returnObj, tracker);
if (!params || params.length == 0) {
return;
}
var infoParam = params[0],
info = {};
if (typeof infoParam === "string") {
info.url = infoParam;
var parsedUrl = url.parse(infoParam);
if (parsedUrl && parsedUrl.hostname) {
info.host = parsedUrl.hostname;
info.port = parsedUrl.port
? parsedUrl.port
: asyncOpnInfo.moduleName === "http"
? 80
: 443;
}
if (parsedUrl && parsedUrl.pathname) {
info.pathname = parsedUrl.pathname;
}
} else if (typeof infoParam === "object") {
info.host = infoParam.host ? infoParam.host : infoParam.hostname;
info.port = infoParam.port
? infoParam.port
: asyncOpnInfo.moduleName === "http"
? 80
: 443;
info.method = infoParam.method;
info.pathname = infoParam.pathname ? infoParam.pathname : "";
info.url = infoParam.host + infoParam.path + info.pathname;
}
tracker.updateInfo(info);
}
/* eslint-disable no-unused-vars */
function registerEvent(original) {
return function (event, listener) {
var curTxn = apmInsightAgentInstance.getCurTxn();
if (!curTxn || curTxn.isCompleted()) {
this._apmInsightAgentTxnContext = null;
return original.apply(this, arguments);
}
this._apmInsightAgentTxnContext = curTxn;
if (!this._apmEmitWrapped && utils.isFunction(this.emit)) {
this.emit = setContext(this.emit);
this._apmEmitWrapped = true;
}
return original.apply(this, arguments);
};
}
/* eslint-enable no-unused-vars */
function setContext(actualListener) {
return function (event) {
var curTxn = this._apmInsightAgentTxnContext;
if (!curTxn || curTxn.isCompleted()) {
return actualListener.apply(this, arguments);
}
const eventName = typeof event == "symbol" ? event.description : event;
var trackerName = "emit - " + eventName;
var trackerInfo = {
trackerName: trackerName,
sync: true,
component: componentName
};
var parentTracker = curTxn.getRootTracker();
var syncOpnInfo = { txn: curTxn, trackerInfo: trackerInfo, syncParentTracker: parentTracker };
return apmwrapper.invokeSyncOpn(
actualListener,
this,
arguments,
syncOpnInfo
);
};
}
function nextTickWrapper(actualNextTick) {
return function (fn) {
var curTxn = apmInsightAgentInstance.getCurTxn();
if (!curTxn || curTxn.isCompleted()) {
return actualNextTick.apply(this, arguments);
}
if (!utils.isFunction(fn)) {
return actualNextTick.apply(this, arguments);
}
var curTracker = apmInsightAgentInstance.getCurTracker();
arguments[0] = nextTickFnWrapper(arguments[0], curTxn, curTracker);
return actualNextTick.apply(this, arguments);
};
}
function nextTickFnWrapper(fn, curTxn, curTracker) {
return function () {
try {
apmInsightAgentInstance.setCurContext(curTxn, curTracker);
return fn.apply(this, arguments);
} finally {
apmInsightAgentInstance.clearCurContext();
}
};
}
module.exports = {
MODULES_INFO: MODULES_INFO,
serverWrap: createServerWrap
};