apminsight
Version:
monitor nodejs applications
403 lines (344 loc) • 15.6 kB
JavaScript
const net = require('net');
const logger = require('./../util/logger');
const constants = require('./../constants');
// Configuration constants
const STATUS_CONNECTION_TIMEOUT = 5000; // 5 seconds
const RECONNECT_DELAY = 2000; // 2 seconds
const DEFAULT_TRACKER_DROP_THRESHOLD = 10; // 10 ms
function DataExporter(config) {
this._config = config;
this._dataConnection = null;
this._isDataConnected = false;
this._reconnectTimer = null;
this._connectPayload = null;
this._applicationInfo = null;
this.dataPort = this._config.getDataExporterDataPort();
this.statusPort = this._config.getDataExporterStatusPort();
}
DataExporter.prototype.init = function () {
this._initDataConnection();
return this;
};
// Status connections will be created on-demand for each request
DataExporter.prototype._createStatusConnection = function (callback) {
const statusConnection = new net.Socket();
const self = this;
let hasResponded = false;
let connectTimeout;
// Set up connection timeout
connectTimeout = setTimeout(function () {
if (!hasResponded) {
hasResponded = true;
logger.warning('Status connection timeout');
statusConnection.destroy();
if (callback) {
callback(null);
}
}
}, STATUS_CONNECTION_TIMEOUT);
statusConnection.connect(
this.statusPort,
this._config.getDataExporterHost()
);
statusConnection.on('data', function (data) {
if (!hasResponded) {
hasResponded = true;
clearTimeout(connectTimeout);
// Call the callback with the response data
if (callback) {
callback(data);
}
// Close the connection after receiving response
statusConnection.end();
}
});
statusConnection.on('close', function () {
if (!hasResponded) {
hasResponded = true;
clearTimeout(connectTimeout);
if (callback) {
callback(null);
}
}
});
statusConnection.on('error', function (err) {
logger.error('Status connection error: ' + err);
if (!hasResponded) {
hasResponded = true;
clearTimeout(connectTimeout);
statusConnection.destroy();
if (callback) {
callback(null);
}
}
});
return statusConnection;
};
DataExporter.prototype._initDataConnection = function () {
if (this._dataConnection) {
return this._dataConnection;
}
this._dataConnection = new net.Socket();
var self = this;
// Connect to data exporter data port
this._dataConnection.connect(
this.dataPort,
this._config.getDataExporterHost(),
function () {
self._isDataConnected = true;
}
);
this._dataConnection.on('close', function () {
logger.debug('Data exporter data connection closed');
self._isDataConnected = false;
});
this._dataConnection.on('error', function (err) {
logger.error('Data exporter data connection error: ', err);
self._isDataConnected = false;
// Check if error is related to connection loss and trigger reconnection
if (constants.connectionErrors.includes(err.code) || self._dataConnection.destroyed) {
logger.warning(`Data connection lost (${err.code})`);
}
});
return this._dataConnection;
};
DataExporter.prototype.sendTxnData = function (txnData) {
// Input validation
if (!txnData) {
logger.warning('Invalid txnData provided to sendTxnData, skipping...');
return;
}
if (!this._isDataConnected || !this._dataConnection || this._dataConnection.destroyed) {
logger.warning('Data exporter data connection not available, skipping txn data, scheduling reconnect...');
this._scheduleReconnect();
return;
}
// Ensure apm object exists before assigning properties
if (!txnData.apm) {
txnData.apm = {};
}
txnData.apm.application_info = this._applicationInfo;
txnData.apm.data = true;
// Handle message preparation and sending
try {
const message = JSON.stringify(txnData); // New line character as required
const encodedMessage = Buffer.from(message).toString('base64') + '\n';
this._dataConnection.write(encodedMessage);
} catch (error) {
logger.error('Error while preparing/sending transaction data: ', error);
// Note: Connection errors are handled asynchronously via the 'error' event handler in _initDataConnection
}
};
DataExporter.prototype.shouldSample = function (reqInfo, callback) {
// Implement sampling logic based on data exporter response
let txnName = reqInfo.uri || "";
// Create default sampling result with default tracker drop threshold
const defaultSamplingResult = {
shouldSample: false,
trackerDropThreshold: DEFAULT_TRACKER_DROP_THRESHOLD
};
// Get a copy of the connectPayload and inject txnName into misc_info
if (this._connectPayload) {
try {
// Create a shallow copy and only copy what we need to modify
let payloadCopy = Object.assign({}, this._connectPayload);
// Ensure misc_info exists and create a copy
payloadCopy.misc_info = Object.assign({}, this._connectPayload.misc_info || {});
// Inject txnName into misc_info using the constant key
payloadCopy.misc_info[constants.s247DataExporterTxnName] = txnName;
// Create a specific callback for sampling response
let samplingResponseCallback = function (data) {
if (!data) {
logger.warning(`No response from data exporter for: ${txnName}`);
callback(defaultSamplingResult);
return;
}
try {
let response = JSON.parse(data.toString());
// Check for instance.status in the response (note: it's a key with dot notation, not nested object)
if (response && response['instance.status'] !== undefined) {
let status = response['instance.status'];
logger.info(`Received instance.status: ${status} from data exporter, for txn: ${txnName}`);
defaultSamplingResult.shouldSample = (status === 911 || status === 920);
// Get the tracker drop threshold from data exporter response
const responseDropThreshold = response['webtransaction.tracker.drop.threshold'];
if (responseDropThreshold !== undefined) {
defaultSamplingResult.trackerDropThreshold = responseDropThreshold;
}
// Get normalized transaction name from data exporter response
const normalizedTxnName = response['normalised.txn.name'];
if (normalizedTxnName !== undefined) {
defaultSamplingResult.normalizedTxnName = normalizedTxnName;
}
// Handle threshold configuration updates from data exporter response
let agentSpecificInfo = {}, customConfigInfo = {};
if (response[constants.logLevel]) {
agentSpecificInfo[constants.logLevel] = response[constants.logLevel];
}
if (response[constants.sqlStracktraceKey]) {
customConfigInfo[constants.sqlStracktraceKey] = response[constants.sqlStracktraceKey];
}
// Update thresholds if any configuration values are present
if (Object.keys(agentSpecificInfo).length > 0 || Object.keys(customConfigInfo).length > 0) {
apmInsightAgentInstance.getThreshold().update(customConfigInfo, agentSpecificInfo);
}
callback(defaultSamplingResult);
} else {
logger.warning(`Received instance.status: not received from data exporter, for txn: ${txnName}`);
if(this._config.isPrintPayloadEnabled()){
logger.debug(`Since instance status is not available, here is the full response: ${JSON.stringify(response)}`);
}
callback(defaultSamplingResult);
}
} catch (error) {
logger.error('Error parsing sampling response from data exporter: ', error);
callback(defaultSamplingResult);
}
};
// Create a new status connection with the sampling response callback
let statusConnection = this._createStatusConnection(samplingResponseCallback);
// Send the request
let message = JSON.stringify(payloadCopy) + '\n'; // New line character as required
statusConnection.write(message);
} catch (error) {
logger.error('Error copying connectPayload in shouldSample: ', error);
callback(defaultSamplingResult);
}
} else {
logger.warning('No connectPayload available for shouldSample, defaulting to false');
callback(defaultSamplingResult);
}
};
DataExporter.prototype.getConnectPayload = function () {
return this._connectPayload;
};
DataExporter.prototype.getApplicationInfo = function () {
return this._applicationInfo;
};
DataExporter.prototype.sendHeartBeat = function () {
// Use the stored connect payload for heartbeat
if (!this._connectPayload) {
logger.warning('No connectPayload available for heartbeat');
return;
}
try {
// Create a specific callback for heartbeat response
let heartbeatResponseCallback = function (data) {
if (!data) {
logger.debug('No response from data exporter for heartbeat');
return;
}
try {
let response = JSON.parse(data.toString());
// logger.debug('Heartbeat response received from data exporter');
// Handle any configuration updates from heartbeat response
if (response) {
let agentSpecificInfo = {}, customConfigInfo = {};
if (response[constants.logLevel]) {
agentSpecificInfo[constants.logLevel] = response[constants.logLevel];
}
if (response[constants.sqlStracktraceKey]) {
customConfigInfo[constants.sqlStracktraceKey] = response[constants.sqlStracktraceKey];
}
// Update thresholds if any configuration values are present
if (Object.keys(agentSpecificInfo).length > 0 || Object.keys(customConfigInfo).length > 0) {
apmInsightAgentInstance.getThreshold().update(customConfigInfo, agentSpecificInfo);
}
}
} catch (error) {
logger.error('Error parsing heartbeat response from data exporter: ', error);
}
};
// Create a new status connection for heartbeat
let statusConnection = this._createStatusConnection(heartbeatResponseCallback);
// Send the heartbeat request using the stored connect payload
let message = JSON.stringify(this._connectPayload) + '\n';
statusConnection.write(message);
if (this._config.isPrintPayloadEnabled()) {
logger.debug('Heartbeat signal sent to data exporter');
}
} catch (error) {
logger.error('Error sending heartbeat to data exporter: ', error);
}
};
DataExporter.prototype.makeConnectRequest = function (payload, callback) {
//This method will be used for first time connect request and maintaining instance id with-us
var self = this;
// Store the payload as JSON object for potential storage on successful connect
try {
this._connectPayload = JSON.parse(payload);
} catch (error) {
logger.error('Error parsing payload in makeConnectRequest: ', error);
this._connectPayload = null;
callback(null);
return;
}
// Create a specific callback for connect response
let connectResponseCallback = function (data) {
if (!data) {
logger.warning('No response from data exporter for connect request');
callback(null);
return;
}
try {
let response = JSON.parse(data.toString());
// Store application info if instance.id is present in response
if (response && response[constants.instanceDotId]) {
self._applicationInfo = {
application_type: constants.applicationType,
application_name: self._config.getApplicationName(),
instance_id: response[constants.instanceDotId]
};
} else {
logger.warning('No instance.id found in response or response is null/undefined. Response: ' + JSON.stringify(response));
}
// Call the callback with the response
callback(response);
} catch (error) {
logger.error('Error parsing connect response from data exporter: ', error);
callback(null);
}
};
// Create a new status connection for this request with connect response callback
let statusConnection = this._createStatusConnection(connectResponseCallback);
// Send the request
let message = payload + '\n'; // New line character as required
statusConnection.write(message);
}
DataExporter.prototype._scheduleReconnect = function () {
var self = this;
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
}
this._config = apmInsightAgentInstance.getConfig();
this._reconnectTimer = setTimeout(function () {
logger.info('Attempting to reconnect to data exporter data connection...');
// Clean up existing data connection before reconnecting
// Status connections are created on-demand, so no cleanup needed for them
if (self._dataConnection) {
self._dataConnection.destroy();
self._dataConnection = null;
}
// Reset connection states (but preserve connectPayload and applicationInfo)
self._isDataConnected = false;
// Reinitialize data connection only
self.init();
}, RECONNECT_DELAY);
};
DataExporter.prototype.close = function () {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
}
// No need to close status connection as they are created on-demand and auto-closed
if (this._dataConnection) {
this._dataConnection.destroy();
this._dataConnection = null;
}
// Reset state
this._isDataConnected = false;
this._connectPayload = null;
this._applicationInfo = null;
};
module.exports = {
DataExporter: DataExporter
};