@instana/collector
Version:
The Instana Node.js metrics and trace data collector
271 lines (242 loc) • 9.51 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2015
*/
;
const {
secrets,
tracing,
util: { ensureNestedObjectExists, configNormalizers }
} = require('@instana/core');
const { constants: tracingConstants } = tracing;
const agentConnection = require('../agentConnection');
const agentOpts = require('../agent/opts');
/** @type {import('@instana/core/src/core').GenericLogger} */
let logger;
/** @type {{ pid: number }} */
let pidStore;
const initialRetryDelay = 10 * 1000; // 10 seconds
const backoffFactor = 1.5;
const maxRetryDelay = 60 * 1000; // one minute
/**
* @typedef {Object} AgentAnnounceResponse
* @property {SecretsConfig} [secrets]
* @property {TracingConfig} [tracing]
* @property {Array.<string>} [extraHeaders]
* @property {boolean|string} [spanBatchingEnabled]
*/
/**
* @typedef {Object} SecretsConfig
* @property {Array.<string>} list
* @property {import('@instana/core/src/util/normalizeConfig').MatchingOption} matcher
*/
/**
* @typedef {Object} TracingConfig
* @property {Array.<string>} [extra-http-headers]
* @property {KafkaTracingConfig} [kafka]
* @property {import('@instana/core/src/tracing').IgnoreEndpoints} [ignore-endpoints]
* @property {boolean} [span-batching-enabled]
* @property {import('@instana/core/src/tracing').Disable} [disable]
*/
/**
* @typedef {Object} KafkaTracingConfig
* @property {boolean} [trace-correlation]
*/
/**
* @param {import('@instana/core/src/util/normalizeConfig').InstanaConfig} config
* @param {any} _pidStore
*/
function init(config, _pidStore) {
logger = config.logger;
pidStore = _pidStore;
}
/**
* @param {import('./').AnnounceCycleContext} ctx
* @param {number} retryDelay
*/
function tryToAnnounce(ctx, retryDelay = initialRetryDelay) {
/** @type {number} */
let nextRetryDelay;
if (retryDelay * backoffFactor >= maxRetryDelay) {
nextRetryDelay = maxRetryDelay;
} else {
nextRetryDelay = retryDelay * backoffFactor;
}
agentConnection.announceNodeCollector((err, rawResponse) => {
if (err) {
logger.info(
`Establishing the connection to the Instana host agent has failed: ${err?.message}. ` +
'This usually means that the Instana host agent is not yet ready to accept connections.' +
`This is not an error. Establishing the connection will be retried in ${retryDelay} ms.`
);
setTimeout(tryToAnnounce, retryDelay, ctx, nextRetryDelay).unref();
return;
}
let agentResponse;
try {
agentResponse = JSON.parse(rawResponse);
} catch (e) {
logger.error(
"Failed to parse the JSON payload from the Instana host agent's response. Establishing the connection" +
`to the Instana host agent will be retried in ${retryDelay} ms. The response payload was ${rawResponse}.` +
`${e?.message} ${e?.stack}`
);
setTimeout(tryToAnnounce, retryDelay, ctx, nextRetryDelay).unref();
return;
}
if (pidStore.pid !== agentResponse.pid) {
logger.info(
`Reporting data to the Instana host agent with the PID from the root namespace (${agentResponse.pid}) ` +
`instead of the in-container PID (${pidStore.pid}).`
);
pidStore.pid = agentResponse.pid;
}
agentOpts.agentUuid = agentResponse.agentUuid;
applyAgentConfiguration(agentResponse);
ctx.transitionTo('announced');
});
}
/**
* @param {AgentAnnounceResponse} agentResponse
*/
function applyAgentConfiguration(agentResponse) {
applySecretsConfiguration(agentResponse);
applyExtraHttpHeaderConfiguration(agentResponse);
applyKafkaTracingConfiguration(agentResponse);
applySpanBatchingConfiguration(agentResponse);
applyIgnoreEndpointsConfiguration(agentResponse);
applyDisableConfiguration(agentResponse);
}
/**
* @param {AgentAnnounceResponse} agentResponse
*/
function applySecretsConfiguration(agentResponse) {
if (agentResponse.secrets) {
if (!(typeof agentResponse.secrets.matcher === 'string')) {
logger.warn(
`Received an invalid secrets configuration from the Instana host agent, attribute matcher is not a string:
${agentResponse.secrets.matcher}`
);
} else if (Object.keys(secrets.matchers).indexOf(agentResponse.secrets.matcher) < 0) {
logger.warn(
`Received an invalid secrets configuration from the Intana agent, matcher is not supported:
${agentResponse.secrets.matcher}`
);
} else if (!Array.isArray(agentResponse.secrets.list)) {
logger.warn(
`Received an invalid secrets configuration from the Instana host agent, attribute list is not an array:
${agentResponse.secrets.list}`
);
} else {
secrets.setMatcher(agentResponse.secrets.matcher, agentResponse.secrets.list);
}
}
}
/**
* @param {AgentAnnounceResponse} agentResponse
*/
function applyExtraHttpHeaderConfiguration(agentResponse) {
if (agentResponse.tracing) {
actuallyApplyExtraHttpHeaderConfiguration(agentResponse.tracing['extra-http-headers']);
return;
}
// Fallback for Node.js discovery prior to version 1.2.18, which did not support providing the complete
// com.instana.tracing section in the agent response. We can remove this legacy fallback approximately in May 2023.
actuallyApplyExtraHttpHeaderConfiguration(agentResponse.extraHeaders);
}
/**
* @param {Array.<string>} extraHeaders
*/
function actuallyApplyExtraHttpHeaderConfiguration(extraHeaders) {
if (Array.isArray(extraHeaders)) {
ensureNestedObjectExists(agentOpts.config, ['tracing', 'http']);
// The Node.js HTTP API converts all incoming HTTP headers into lowercase.
agentOpts.config.tracing.http.extraHttpHeadersToCapture = extraHeaders.map((/** @type {string} */ s) =>
s.toLowerCase()
);
}
}
/**
* @param {AgentAnnounceResponse} agentResponse
*/
function applyKafkaTracingConfiguration(agentResponse) {
if (agentResponse.tracing && agentResponse.tracing.kafka) {
const kafkaTracingConfigFromAgent = agentResponse.tracing.kafka;
const kafkaTracingConfig = {
traceCorrelation:
kafkaTracingConfigFromAgent['trace-correlation'] != null
? kafkaTracingConfigFromAgent['trace-correlation']
: tracingConstants.kafkaTraceCorrelationDefault
};
ensureNestedObjectExists(agentOpts.config, ['tracing', 'kafka']);
agentOpts.config.tracing.kafka = kafkaTracingConfig;
}
// There is no fallback because there is are no legacy agent response attributes for the Kafka tracing config, those
// were only introduced with the Node.js discovery version 1.2.18.
}
/**
* @param {AgentAnnounceResponse} agentResponse
*/
function applySpanBatchingConfiguration(agentResponse) {
if (agentResponse.tracing) {
if (agentResponse.tracing['span-batching-enabled'] === true) {
ensureNestedObjectExists(agentOpts.config, ['tracing']);
agentOpts.config.tracing.spanBatchingEnabled = true;
}
return;
}
// Fallback for Node.js discovery prior to version 1.2.18, which did not sent the span-batching-enabled config in the
// common tracing options section. We can remove this legacy fallback approximately in May 2023.
if (agentResponse.spanBatchingEnabled === true || agentResponse.spanBatchingEnabled === 'true') {
logger.info('Enabling span batching via Instana host agent configuration.');
ensureNestedObjectExists(agentOpts.config, ['tracing']);
agentOpts.config.tracing.spanBatchingEnabled = true;
}
}
/**
* The incoming agent configuration may include strings, an array of strings, or objects
* that define filtering criteria.
*
* - Phase 1: Introduced simple filtering based on operations (e.g. redis.get).
* For more information see: https://github.ibm.com/instana/requests-for-discussion/pull/84
*
* - Phase 2: Added advanced filtering with 'methods' and 'endpoints'.
* For more inormation see: https://github.ibm.com/instana/technical-documentation/pull/333
*
* The normalized internal format is:
* { [serviceName: string]: [{ methods: string[], endpoints: string[] }] }
*
* @param {AgentAnnounceResponse} agentResponse
*/
function applyIgnoreEndpointsConfiguration(agentResponse) {
const ignoreEndpointsConfig = agentResponse?.tracing?.['ignore-endpoints'];
if (!ignoreEndpointsConfig) return;
ensureNestedObjectExists(agentOpts.config, ['tracing', 'ignoreEndpoints']);
agentOpts.config.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.normalizeConfig(ignoreEndpointsConfig);
}
/**
* The incoming agent configuration include `disable` object that include
* which instrumentation/categories should be disabled. For example: { logging: true, console: false }
* This will be normalized into an array of strings, such as: ['logging', '!console']
* For more details on the design, refer to:https://github.ibm.com/instana/technical-documentation/pull/344
*
* @param {AgentAnnounceResponse} agentResponse
*/
function applyDisableConfiguration(agentResponse) {
const disablingConfig = agentResponse?.tracing?.disable;
if (!disablingConfig) return;
ensureNestedObjectExists(agentOpts.config, ['tracing', 'disable']);
agentOpts.config.tracing.disable = configNormalizers.disable.normalizeExternalConfig({
tracing: { disable: disablingConfig }
});
}
module.exports = {
init,
/**
* @param {import('./').AnnounceCycleContext} ctx
*/
enter: function (ctx) {
tryToAnnounce(ctx);
},
leave: function () {}
};