UNPKG

@instana/core

Version:
414 lines (368 loc) 14.7 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2016 */ 'use strict'; const crypto = require('crypto'); const path = require('path'); const StringDecoder = require('string_decoder').StringDecoder; const stackTrace = require('../util/stackTrace'); const { DEFAULT_STACK_TRACE_LENGTH, DEFAULT_STACK_TRACE_MODE, STACK_TRACE_MODES } = require('../util/constants'); /** @type {import('../core').GenericLogger} */ let logger; const hexDecoder = new StringDecoder('hex'); /** * @type {number} */ let stackTraceLength; /** * @type {string} */ // eslint-disable-next-line no-unused-vars let stackTraceMode; /** * @param {import('../config').InstanaConfig} config */ exports.init = function (config) { logger = config.logger; stackTraceLength = config?.tracing?.stackTraceLength; stackTraceMode = config?.tracing?.stackTrace; }; /** * @param {import('@instana/collector/src/types/collector').AgentConfig} extraConfig */ exports.activate = function activate(extraConfig) { const agentTraceConfig = extraConfig?.tracing; // Note: We check whether the already-initialized stackTraceLength equals the default value. // If it does, we can safely override it, since the user did not explicitly configure it. // Note: If the user configured a value via env or code and also configured a different value in the agent, // but the env/code value happens to equal the default, the agent value would overwrite it. // This is a rare edge case and acceptable for now. if (agentTraceConfig?.stackTrace && stackTraceMode === DEFAULT_STACK_TRACE_MODE) { stackTraceMode = agentTraceConfig.stackTrace; } // stackTraceLength is valid when set to any number, including 0 if (agentTraceConfig?.stackTraceLength != null && stackTraceLength === DEFAULT_STACK_TRACE_LENGTH) { stackTraceLength = agentTraceConfig.stackTraceLength; } }; /** * @param {Function} referenceFunction * @param {number} [drop] * @returns {Array.<*>} */ exports.getStackTrace = function getStackTrace(referenceFunction, drop) { if (stackTraceMode === STACK_TRACE_MODES.NONE || stackTraceMode === STACK_TRACE_MODES.ERROR) { return []; } return stackTrace.captureStackTrace(stackTraceLength, referenceFunction, drop); }; exports.generateRandomTraceId = function generateRandomTraceId() { // Maintenance note (128-bit-trace-ids): As soon as all Instana tracers support 128 bit trace IDs we can generate a // string of length 32 here. return exports.generateRandomId(16); }; exports.generateRandomLongTraceId = function generateRandomLongTraceId() { return exports.generateRandomId(32); }; exports.generateRandomSpanId = function generateRandomSpanId() { return exports.generateRandomId(16); }; /** * @param {number} length * @returns {string} */ exports.generateRandomId = function (length) { return crypto .randomBytes(Math.ceil(length / 2)) .toString('hex') .slice(0, length); }; /** * @param {Buffer} buffer * @returns {{ * t?: string, * s?: string * }} */ exports.readTraceContextFromBuffer = function readTraceContextFromBuffer(buffer) { if (!Buffer.isBuffer(buffer)) { logger.error(`Not a buffer: ${buffer}`); return {}; } if (buffer.length !== 24) { logger.error(`Only buffers of length 24 are supported: ${buffer}`); return {}; } // Check if the first 8 bytes are all zeroes: // (Beginning with Node.js 12, this check could be simply `buffer.readBigInt64BE(0) !== 0n) {`.) if (buffer.readInt32BE(0) !== 0 || buffer.readInt32BE(4) !== 0) { return { t: readHexFromBuffer(buffer, 0, 16), s: readHexFromBuffer(buffer, 16, 8) }; } else { return { t: readHexFromBuffer(buffer, 8, 8), s: readHexFromBuffer(buffer, 16, 8) }; } }; /** * @param {Buffer} buffer * @param {number} offset * @param {number} length * @returns {string} */ function readHexFromBuffer(buffer, offset, length) { return hexDecoder.write(buffer.subarray(offset, offset + length)); } /** * @param {string} hexString * @param {Buffer} buffer * @param {number} offsetFromRight * @returns {Buffer} */ exports.unsignedHexStringToBuffer = function unsignedHexStringToBuffer(hexString, buffer, offsetFromRight) { /** @type {number} */ let offset; if (buffer && offsetFromRight != null) { offset = buffer.length - hexString.length / 2 - offsetFromRight; } else { offset = 0; } if (hexString.length === 16) { buffer = buffer || Buffer.alloc(8); } else if (hexString.length === 32) { buffer = buffer || Buffer.alloc(16); } else { logger.error(`Only hex strings of lengths 16 or 32 can be converted, received: ${hexString}`); return buffer; } writeHexToBuffer(hexString, buffer, offset); return buffer; }; /** * @param {string} traceId * @param {string} spanId * @returns {Buffer} */ exports.unsignedHexStringsToBuffer = function unsignedHexStringsToBuffer(traceId, spanId) { const buffer = Buffer.alloc(24); exports.unsignedHexStringToBuffer(traceId, buffer, 8); exports.unsignedHexStringToBuffer(spanId, buffer, 0); return buffer; }; /** * Writes characters from a hex string directly to a buffer. The buffer will contain a binary representation of the * given hex string after this operation. No length checks are executed, so the caller is responsible for writing within * the bounds of the given buffer. * * The string hexString must only contain the characters [0-9a-f]. * @param {string} hexString * @param {Buffer} buffer * @param {number} offset */ function writeHexToBuffer(hexString, buffer, offset) { // This implementation uses Node.js buffer internals directly: // https://github.com/nodejs/node/blob/92cef79779d121d934dcb161c068bdac35e6a963/lib/internal/buffer.js#L1005 -> // https://github.com/nodejs/node/blob/master/src/node_buffer.cc#L1196 / // https://github.com/nodejs/node/blob/master/src/node_buffer.cc#L681 // @ts-ignore buffer.hexWrite(hexString, offset, hexString.length / 2); } /** * @param {import('../core').InstanaBaseSpan} span * @returns {Buffer} */ exports.renderTraceContextToBuffer = function renderTraceContextToBuffer(span) { return exports.unsignedHexStringsToBuffer(span.t, span.s); }; /** * @param {string} stmt * @returns {string} */ exports.shortenDatabaseStatement = function shortenDatabaseStatement(stmt) { if (stmt == null || typeof stmt !== 'string') { return undefined; } return stmt.substring(0, 4000); }; /** * @param {string} connectionStr * @returns {string} */ exports.sanitizeConnectionStr = function sanitizeConnectionStr(connectionStr) { if (connectionStr == null || typeof connectionStr !== 'string') { return undefined; } const replaced = connectionStr.replace(/PWD\s*=\s*[^;]*/, 'PWD=<redacted>'); return replaced; }; /** * Iterates over all attributes of the given object and returns the first attribute for which the name matches the given * name in a case insensitive fashion, or null if no such attribute exists. * * @param {*} object * @param {string} key * @returns {*} */ exports.readAttribCaseInsensitive = function readAttribCaseInsensitive(object, key) { if (!object || typeof object !== 'object' || typeof key !== 'string') { return null; } if (object[key]) { // fast path for cases where case insensitive search is not required return object[key]; } const keyUpper = key.toUpperCase(); const allKeys = Object.keys(object); for (let i = 0; i < allKeys.length; i++) { if (typeof allKeys[i] === 'string' && allKeys[i].toUpperCase() === keyUpper) { return object[allKeys[i]]; } } return null; }; /** * In rare cases, we need to require a module from dependencies of the application under test, most notably specific * modules from packages that we instrument. This works without issues when the application under test has installed * @instana/collector and friends as a normal dependency, because then our packages are located in the same node_modules * folder and our instrumentation module will have a module load path list * (see https://nodejs.org/api/modules.html#modulepaths) that includes the application's node_modules folder. * * The situation is different when we are loaded via "--require" from a global installation -- this is the norm for * @instana/aws-fargate, @instana/google-cloud-run, the Kubernetes autotrace webhook * (https://www.ibm.com/docs/en/instana-observability/current?topic=kubernetes-instana-autotrace-webhook) or when * using the global installation pattern for @instana/collector that does not require modifying the source code see( * https://www.ibm.com/docs/en/instana-observability/current?topic=nodejs-collector-installation#installing-the- * collector-globally). * In these scenarios, the module load path list does not include the application's node_module folder. Thus we need to * be a bit more clever when requiring a module from the application's dependencies. We solve this by constructing the * file system path of the desired module by using a known path from a module of the same package and the relative path * from that module to the module we need to load. Note that instrumentations which use the requireHook module can * obtain the base path via their onModuleLoad/onFileLoad callbacks. * * @param {string} basePath the absolute file system path of a module that is close to the one that should be loaded * @param {[string]} relativePath the relative path from the basePath to the desired module * @returns {*} the requested module */ exports.requireModuleFromApplicationUnderMonitoringSafely = function requireModuleFromApplicationUnderMonitoringSafely( basePath, ...relativePath ) { return require(path.join(basePath, ...relativePath)); }; exports.findCallback = (/** @type {string | any[]} */ originalArgs) => { let originalCallback; let callbackIndex = -1; // CASE: libraries pass a class into a function as argument const isClass = (/** @type {any} */ fn) => { return typeof fn === 'function' && /^\s*class\s+/.test(Function.prototype.toString.call(fn)); }; // If there is any function that takes two or more functions as an argument, // the convention would be to pass in the callback as the last argument, thus searching // from the end backwards might be marginally safer. for (let i = originalArgs.length - 1; i >= 0; i--) { if (typeof originalArgs[i] === 'function' && !isClass(originalArgs[i])) { originalCallback = originalArgs[i]; callbackIndex = i; break; } } return { originalCallback, callbackIndex }; }; exports.extractErrorMessage = (/** @type {any} */ normalizedError) => { // If error has cause property that is an Error, we want to extract the message from the cause // Reference Node.js error cause: https://nodejs.org/api/errors.html#errorcause if (normalizedError?.cause instanceof Error) { normalizedError = normalizedError.cause; } if (normalizedError?.details) { return `${normalizedError.name || 'Error'}: ${normalizedError.details}`; } else if (normalizedError?.message) { return `${normalizedError.name || 'Error'}: ${normalizedError.message}`; } else { return normalizedError?.code || 'No error message found.'; } }; /** * Sets error details on a span for a specific technology. * Handles different error formats: strings, objects with details/message/code properties. * Supports nested paths for SDK spans via dot-separated strings or arrays. * * Examples: * - setErrorDetails(span, error, 'nats') // flat key * - setErrorDetails(span, error, 'sdk.custom.tags.message') // dot-separated string * - setErrorDetails(span, error, ['sdk', 'custom', 'tags', 'message']) // array path * * @param {import('../core').InstanaBaseSpan} span - The span to update * @param {Error | string | Object} error - The error object, error string, or object with error properties * @param {string | Array<string>} technology - The technology name or nested path */ exports.setErrorDetails = function setErrorDetails(span, error, technology) { try { if (!error) { return; } // Normalize error to object format at the beginning /** @type {{ * message?: string, * stack?: string | null, * name?: string, * code?: string, * details?: string, * cause?: any * }} */ let normalizedError; if (typeof error === 'string') { normalizedError = { message: error, stack: null }; } else { normalizedError = error; } const errorMessage = exports.extractErrorMessage(normalizedError); let errorPath = null; if (Array.isArray(technology)) { errorPath = technology; } else if (typeof technology === 'string' && technology.includes('.')) { errorPath = technology.split('.'); } if (errorPath) { let target = span.data; // Traverse the object path and create missing nested objects along the way // Without this, deeper properties would fail to assign if their parent objects don't exist for (let i = 0; i < errorPath.length - 1; i++) { const key = errorPath[i]; if (!target[key]) { target[key] = {}; } target = target[key]; } const errorKey = errorPath[errorPath.length - 1]; if (!target[errorKey]) { target[errorKey] = errorMessage.substring(0, 200); } } else if (typeof technology === 'string' && technology && span.data?.[technology]) { if (!span.data[technology].error) { span.data[technology].error = errorMessage.substring(0, 200); } } // If the mode is none, we set span.data[technology].error (as done above) and return immediately if (stackTraceMode === STACK_TRACE_MODES.NONE) { return; } // If stack trace collection is set to 'error' or 'all' and an error occurs, // generate a stack trace from the error and overwrite any existing stack. // If the error has a `cause` property and it is an Error instance, prefer // the cause’s stack trace, as it represents the root cause. // See: https://nodejs.org/api/errors.html#errorcause const stackToUse = (normalizedError?.cause instanceof Error && normalizedError.cause.stack) || normalizedError.stack; if (stackToUse) { const stackArray = stackTrace.parseStackTraceFromString(stackToUse); span.stack = stackArray.length > 0 ? stackArray.slice(0, stackTraceLength) : span.stack || []; } else { span.stack = span.stack || []; } } catch (err) { logger.error('Failed to set error details on span:', err); } };