@instana/collector
Version:
The Instana Node.js metrics and trace data collector
271 lines (234 loc) • 8.34 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2015
*/
;
let isMainThread = true;
try {
isMainThread = require('worker_threads').isMainThread;
} catch (err) {
// Worker threads are not available, so we know that this is the main thread.
}
const { tracing } = require('@instana/core');
const agentConnection = require('../agentConnection');
const agentOpts = require('../agent/opts');
const initializedTooLate = require('../util/initializedTooLate');
const metrics = require('../metrics');
const requestHandler = require('../agent/requestHandler');
const transmissionCycle = require('../metrics/transmissionCycle');
const uncaught = require('../uncaught');
const { isNodeVersionEOL } = require('../util/eol');
const ONE_MINUTE = 60 * 1000;
const EOL_EVENT_REFRESH_INTERVAL = 6 * 60 * ONE_MINUTE; // 6 hours
const EOL_EVENT_DURATION = 6 * 60 * ONE_MINUTE + ONE_MINUTE; // 6 hours + 1 minute
const FIRE_MONITORING_EVENT_DURATION_IN_MS = process.env.INSTANA_FIRE_MONITORING_EVENT_DURATION_IN_MS
? Number(process.env.INSTANA_FIRE_MONITORING_EVENT_DURATION_IN_MS)
: 600000;
/** @type {*} */
let autoprofile;
/** @type {*} */
let profiler;
/** @type {import('@instana/core/src/core').GenericLogger} */
let logger;
/** @type {import('./').AnnounceCycleContext} */
let ctx;
let tracingMetricsDelay = 1000;
if (typeof process.env.INSTANA_TRACER_METRICS_INTERVAL === 'string') {
tracingMetricsDelay = parseInt(process.env.INSTANA_TRACER_METRICS_INTERVAL, 10);
if (isNaN(tracingMetricsDelay) || tracingMetricsDelay <= 0) {
tracingMetricsDelay = 1000;
}
}
/** @type {NodeJS.Timeout} */
let tracingMetricsTimeout = null;
/** @type {{ pid: number, getEntityId: Function }} */
let pidStore;
/**
* @type {boolean}
*/
let disableEOLEvents;
/**
* @param {import('@instana/core/src/util/normalizeConfig').InstanaConfig} config
* @param {any} _pidStore
*/
function init(config, _pidStore) {
logger = config.logger;
pidStore = _pidStore;
disableEOLEvents = config.tracing?.disableEOLEvents;
initializedTooLate.init(config);
// TODO: Why is autoProfile part of agentOpts? O_o Please refactor this away in next major release.
if (agentOpts.autoProfile) {
try {
// @ts-ignore - TS cannot find @instana/profile.
// TODO: @instana/autoprofile is not linted or typed
// eslint-disable-next-line import/no-extraneous-dependencies
autoprofile = require('@instana/autoprofile');
} catch (e) {
logger.info(
'Could not load @instana/autoprofile. You will not get profiling information for this Node.js app in Instana,' +
'although autoprofiling has been enabled. This typically occurs when native addons could not be built ' +
'during module installation (npm install/yarn) or when npm install --no-optional or yarn --ignore-optional ' +
'have been used to install dependencies. See the instructions to learn more about the requirements of the ' +
'collector: ' +
'https://www.ibm.com/docs/en/instana-observability/current?topic=nodejs-collector-installation#native-add-ons'
);
fireMonitoringEvent();
setInterval(fireMonitoringEvent, FIRE_MONITORING_EVENT_DURATION_IN_MS).unref();
}
}
}
/**
* @param {import('./').AnnounceCycleContext} _ctx
*/
function enter(_ctx) {
ctx = _ctx;
initializedTooLate.check();
logger.debug(`isMainThread: ${isMainThread}`);
if (isMainThread) {
uncaught.activate();
metrics.activate();
requestHandler.activate();
transmissionCycle.activate(
metrics,
agentConnection,
/**
* @param {Array.<import('../agent/requestHandler').AnnounceRequest>} requests
*/
function onSuccess(requests) {
requestHandler.handleRequests(requests);
},
function onError() {
ctx.transitionTo('unannounced');
}
);
scheduleTracingMetrics();
if (!disableEOLEvents) {
detectEOLNodeVersion();
}
}
tracing.activate(agentOpts.config);
if (agentOpts.autoProfile && autoprofile) {
profiler = autoprofile.start();
/**
* @param {*} profiles
* @param {(...args: *) => *} callback
*/
profiler.sendProfiles = (profiles, callback) => {
agentConnection.sendProfiles(profiles, callback);
};
profiler.getExternalPid = () => pidStore.pid;
profiler.getLogger = () => logger;
profiler.start();
}
logger.info('The Instana Node.js collector is now fully initialized and connected to the Instana host agent.');
// CASE: This is an IPC message only for a parent process.
// TODO: Add an EventEmitter functionality for the current process
// such as `instana.on('instana.collector.initialized')`.
// eslint-disable-next-line no-unused-expressions
process.send && process.send('instana.collector.initialized');
if (!isMainThread) {
const { parentPort } = require('worker_threads');
if (parentPort) {
// CASE: This is for the worker thread if available.
parentPort.postMessage('instana.collector.initialized');
}
}
}
function leave() {
if (isMainThread) {
uncaught.deactivate();
metrics.deactivate();
requestHandler.deactivate();
transmissionCycle.deactivate();
deScheduleTracingMetrics();
}
tracing.deactivate();
if (profiler) {
profiler.destroy();
profiler = null;
}
}
function sendTracingMetrics() {
const payload = tracing._getAndResetTracingMetrics();
agentConnection.sendTracingMetricsToAgent(payload, error => {
if (error) {
logger.info(
`Error received while trying to send tracing metrics to agent: ${error?.message}.` +
' This will not affect monitoring or tracing.'
);
if (typeof error.message === 'string' && error.message.indexOf('Got status code 404')) {
logger.info(
'Apparently the version of the Instana host agent on this host does not support the POST /tracermetrics ' +
'endpoint, will stop sending tracing metrics.'
);
return;
}
}
scheduleTracingMetrics();
});
}
function scheduleTracingMetrics() {
tracingMetricsTimeout = setTimeout(sendTracingMetrics, tracingMetricsDelay);
tracingMetricsTimeout.unref();
}
function deScheduleTracingMetrics() {
if (tracingMetricsTimeout) {
clearTimeout(tracingMetricsTimeout);
tracingMetricsTimeout = null;
}
}
function fireMonitoringEvent() {
agentConnection.sendAgentMonitoringEvent('nodejs_collector_native_addon_autoprofile_missing', 'PROFILER', error => {
if (error) {
logger.error(
`Error received while trying to send a monitoring event to the Instana host agent: ${error?.message}`
);
}
});
}
function sendEOLEvent() {
const pid = pidStore.getEntityId();
agentConnection.sendEvent(
{
title: `Node.js version ${process.versions.node} reached its end of life`,
text:
'This version no longer receives updates or security fixes and might contain unfixed vulnerabilities.\n\n' +
'Please consider upgrading Node.js to an active version.\n\n' +
'For a list of active versions visit ' +
'[https://nodejs.org/en/about/releases/](https://nodejs.org/en/about/releases/)',
plugin: 'com.instana.forge.infrastructure.runtime.nodejs.NodeJsRuntimePlatform',
id: pid,
timestamp: Date.now(),
duration: EOL_EVENT_DURATION,
severity: agentConnection.AgentEventSeverity.WARNING,
path: `${agentOpts.agentUuid}/${pid}/nodejs-eol`
},
err => {
if (err) {
logger.debug(
`Sending a monitoring event for the Node.js version end-of-life check has failed.
${err?.message} ${err?.stack}`
);
}
}
);
}
/**
* Sends an issue event to the agent when the Node.js version has reached end of life.
* It will work for non-serverless environments where an agent is present.
* (At the time of writing, the backend service that our serverless in-process collectors send data to does not have
* support for events.)
*/
function detectEOLNodeVersion() {
if (isNodeVersionEOL()) {
setTimeout(() => {
sendEOLEvent();
setInterval(sendEOLEvent, EOL_EVENT_REFRESH_INTERVAL).unref();
}, 2000);
}
}
module.exports = exports = {
init,
enter,
leave
};