UNPKG

apminsight

Version:

monitor nodejs applications

403 lines (344 loc) 15.6 kB
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 };