UNPKG

@instana/core

Version:
478 lines (410 loc) 14.8 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2021 */ 'use strict'; const shimmer = require('../../../../shimmer'); const cls = require('../../../../cls'); const { configureEntrySpan, hasTracingAttributes, logTooManyAttributesWarningOnce, readTracingAttributesFromSns, readTracingAttributes } = require('../aws_utils'); const { ENTRY, EXIT, sqsAttributeNames } = require('../../../../constants'); const hook = require('../../../../../util/hook'); const tracingUtil = require('../../../../tracingUtil'); // Available call types to be sent into span.data.sqs.type const callTypes = { CREATE_QUEUE: 'create.queue', GET_QUEUE: 'get.queue', SEND_MESSAGE: 'single.sync', SEND_MESSAGE_BATCH: 'batch.sync', DELETE_MESSAGE: 'delete.single.sync', DELETE_MESSAGE_BATCH: 'delete.batch.sync' }; const sortTypes = { ENTRY: 'entry', EXIT: 'exit' }; let logger; let isActive = false; exports.init = function init(config) { logger = config.logger; hook.onModuleLoad('aws-sdk', instrumentSQS); hook.onModuleLoad('sqs-consumer', instrumentSQSConsumer); }; function instrumentSQS(AWS) { // /aws-sdk/lib/service.js#defineMethods shimmer.wrap(AWS.Service, 'defineMethods', function (original) { return function (svc) { const patchedMethod = original.apply(this, arguments); if ( svc && svc.prototype && typeof svc.prototype.serviceIdentifier === 'string' && svc.prototype.serviceIdentifier.toLowerCase() === 'sqs' ) { shimmer.wrap(svc.prototype, 'sendMessage', shimSendMessage); shimmer.wrap(svc.prototype, 'sendMessageBatch', shimSendMessage); shimmer.wrap(svc.prototype, 'receiveMessage', shimReceiveMessage); } return patchedMethod; }; }); } function shimSendMessage(originalSendMessage) { return function () { const originalArgs = new Array(arguments.length); for (let i = 0; i < originalArgs.length; i++) { originalArgs[i] = arguments[i]; } return instrumentedSendMessage(this, originalSendMessage, originalArgs); }; } function instrumentedSendMessage(ctx, originalSendMessage, originalArgs) { /** * Send Message Attribues format * { * ... * MessageAttributes: { * CustomAttribute: { * DataType: 'String', * StringValue: 'Custom Value' * } * } * } */ const messageData = originalArgs[0]; if (!messageData) { return originalSendMessage.apply(ctx, originalArgs); } let attributes; const isBatch = messageData.Entries && messageData.Entries.length > 0; if (isBatch) { messageData.Entries.forEach(entry => { if (!entry.MessageAttributes) { entry.MessageAttributes = {}; } }); } else { attributes = messageData.MessageAttributes; } if (!attributes && !isBatch) { attributes = messageData.MessageAttributes = {}; } const skipTracingResult = cls.skipExitTracing({ isActive, extendedResponse: true }); if (skipTracingResult.skip) { if (skipTracingResult.suppressed) { if (isBatch) { messageData.Entries.forEach(entry => { propagateSuppression(entry.MessageAttributes); }); } else { propagateSuppression(attributes); } } return originalSendMessage.apply(ctx, originalArgs); } return cls.ns.runAndReturn(() => { const span = cls.startSpan({ spanName: 'sqs', kind: EXIT }); span.ts = Date.now(); span.stack = tracingUtil.getStackTrace(instrumentedSendMessage); span.data.sqs = { sort: sortTypes.EXIT, type: messageData.Entries ? callTypes.SEND_MESSAGE_BATCH : callTypes.SEND_MESSAGE, group: messageData.MessageGroupId, queue: originalArgs[0].QueueUrl || '' }; if (isBatch) { span.data.sqs.size = messageData.Entries.length; messageData.Entries.forEach(entry => { propagateTraceContext(entry.MessageAttributes, span); }); } else { propagateTraceContext(attributes, span); } const originalCallback = originalArgs[1]; if (typeof originalCallback === 'function') { originalArgs[1] = cls.ns.bind(function (err, data) { finishSpan(err, data, span); originalCallback.apply(this, arguments); }); } const awsRequest = originalSendMessage.apply(ctx, originalArgs); if (typeof awsRequest.promise === 'function') { awsRequest.promise = cls.ns.bind(awsRequest.promise); } // this is what the promise actually does awsRequest.on('complete', function onComplete(data) { if (data && data.error) { finishSpan(data.error, null, span); throw data.error; } else { finishSpan(null, data, span); return data; } }); return awsRequest; }); } function propagateSuppression(attributes) { if (!attributes || typeof attributes !== 'object') { return; } // SQS has a limit of 10 message attributes, we would need to add one attribute. if (Object.keys(attributes).length >= 10) { logTooManyAttributesWarningOnce(logger, attributes, 1); return; } attributes[sqsAttributeNames.LEVEL] = { DataType: 'String', StringValue: '0' }; } function propagateTraceContext(attributes, span) { if (!attributes || typeof attributes !== 'object') { return; } // SQS has a limit of 10 message attributes, we need to add two attributes. if (Object.keys(attributes).length >= 9) { logTooManyAttributesWarningOnce(logger, attributes, 2); return; } attributes[sqsAttributeNames.TRACE_ID] = { DataType: 'String', StringValue: span.t }; attributes[sqsAttributeNames.SPAN_ID] = { DataType: 'String', StringValue: span.s }; } function shimReceiveMessage(originalReceiveMessage) { return function () { if (isActive) { const parentSpan = cls.getCurrentSpan(); if (parentSpan) { logger.warn( // eslint-disable-next-line max-len `Cannot start an AWS SQS entry span when another span is already active. Currently, the following span is active: ${JSON.stringify( parentSpan )}` ); return originalReceiveMessage.apply(this, arguments); } const originalArgs = new Array(arguments.length); for (let i = 0; i < originalArgs.length; i++) { originalArgs[i] = arguments[i]; } return instrumentedReceiveMessage(this, originalReceiveMessage, originalArgs); } return originalReceiveMessage.apply(this, arguments); }; } function instrumentedReceiveMessage(ctx, originalReceiveMessage, originalArgs) { return cls.ns.runAndReturn(() => { const span = cls.startSpan({ spanName: 'sqs', kind: ENTRY }); span.stack = tracingUtil.getStackTrace(instrumentedSendMessage); span.data.sqs = { sort: sortTypes.ENTRY, queue: originalArgs[0].QueueUrl || '' }; /** * The MessageAttributeNames attribute is an option that you tell which message attributes you want to see. * As we use message attributes to store Instana headers, if the customer does not set this attribute to All, * we cannot see the Instana headers, so we need to explicitly add them. */ const receveingParams = originalArgs[0]; if (!receveingParams.MessageAttributeNames) { receveingParams.MessageAttributeNames = []; } if ( !receveingParams.MessageAttributeNames.includes('X_INSTANA*') && !receveingParams.MessageAttributeNames.includes('All') ) { receveingParams.MessageAttributeNames.push('X_INSTANA*'); } // callback use case const originalCallback = originalArgs[1]; if (typeof originalCallback === 'function') { originalArgs[1] = cls.ns.bind(function (err, data) { if (err) { addErrorToSpan(err, span); setImmediate(() => finishSpan(null, null, span)); return originalCallback.apply(this, arguments); } else if (data && data.error) { addErrorToSpan(data.err, span); setImmediate(() => finishSpan(null, null, span)); return originalCallback.apply(this, arguments); } else if (data && data.Messages && data.Messages.length > 0) { let tracingAttributes = readTracingAttributes(data.Messages[0].MessageAttributes); if (!hasTracingAttributes(tracingAttributes)) { tracingAttributes = readTracingAttributesFromSns(data.Messages[0].Body); } if (tracingAttributes.level === '0') { cls.setTracingLevel('0'); setImmediate(() => span.cancel()); return originalCallback.apply(this, arguments); } configureEntrySpan(span, data, tracingAttributes); setImmediate(() => finishSpan(null, data, span)); return originalCallback.apply(this, arguments); } else { // No messages have been received. The assumption is that no follow-up activities will occur, but polling for // messages might be triggered again in the same event loop tick. Thus we also need to cancel the span // _synchronously_ in the same event loop tick. See commit message for details. span.cancel(); return originalCallback.apply(this, arguments); } }); } const awsRequest = originalReceiveMessage.apply(ctx, originalArgs); // promise use case if (typeof awsRequest.promise === 'function' && typeof originalCallback !== 'function') { const originalPromiseFn = awsRequest.promise; awsRequest.promise = cls.ns.bind(function () { const promise = originalPromiseFn.apply(awsRequest, arguments); promise.then( data => { if (data && data.error) { addErrorToSpan(data.error, span); setImmediate(() => finishSpan(null, null, span)); return data; } else if (data && data.Messages && data.Messages.length > 0) { let tracingAttributes = readTracingAttributes(data.Messages[0].MessageAttributes); if (!hasTracingAttributes(tracingAttributes)) { tracingAttributes = readTracingAttributesFromSns(data.Messages[0].Body); } if (tracingAttributes.level === '0') { cls.setTracingLevel('0'); setImmediate(() => span.cancel()); return data; } configureEntrySpan(span, data, tracingAttributes); setImmediate(() => finishSpan(null, data, span)); } else { // No messages have been received. The assumption is that no follow-up activities will occur, but polling // for messages might be triggered again in the same event loop tick. Thus we also need to cancel the span // _synchronously_ in the same event loop tick. See commit message for details. span.cancel(); } return data; }, error => { addErrorToSpan(error, span); setImmediate(() => finishSpan(null, null, span)); throw error; } ); promise.instanaAsyncContext = cls.getAsyncContext(); if (originalArgs[0] && originalArgs[0].QueueUrl) { ctx[originalArgs[0].QueueUrl] = promise.instanaAsyncContext; } // Usually, native promises are handled automatically, that is, their then/catch/finally is executed in the CLS // context they have been created in. Apparently, here the promise is created outside the CLS context, thus we // bind the handlers manually. promise.then = cls.ns.bind(promise.then); if (promise.catch) { promise.catch = cls.ns.bind(promise.catch); } if (promise.finally) { promise.finally = cls.ns.bind(promise.finally); } return promise; }); } return awsRequest; }); } function finishSpan(err, data, span) { if (err) { addErrorToSpan(err, span); } if (typeof data === 'string') { span.data.sqs.messageId = data; } span.d = Date.now() - span.ts; span.transmit(); } function addErrorToSpan(err, span) { if (err) { span.ec = 1; span.data.sqs.error = err.message || err.code || JSON.stringify(err); } } exports.activate = function activate() { isActive = true; }; exports.deactivate = function deactivate() { isActive = false; }; /* *********** SQS Consumer ************** */ function instrumentSQSConsumer(SQSConsumer) { shimmer.wrap(SQSConsumer.Consumer.prototype, 'receiveMessage', shimSQSConsumerReceiveMessage); shimmer.wrap(SQSConsumer.Consumer.prototype, 'executeHandler', shimSQSConsumerExecuteHandler); shimmer.wrap(SQSConsumer.Consumer.prototype, 'executeBatchHandler', shimSQSConsumerExecuteHandler); } function shimSQSConsumerExecuteHandler(original) { return function () { if (isActive) { const originalArgs = new Array(arguments.length); for (let i = 0; i < originalArgs.length; i++) { originalArgs[i] = arguments[i]; } return instrumentedSQSConsumerExecuteHandler(this, original, originalArgs); } return original.apply(this, arguments); }; } function instrumentedSQSConsumerExecuteHandler(ctx, original, originalArgs) { const instanaAsyncContext = ctx.sqs[ctx._instanaSqsQueueUrl]; delete ctx.sqs[ctx._instanaSqsQueueUrl]; if (instanaAsyncContext) { return cls.runInAsyncContext(instanaAsyncContext, () => { const span = cls.getCurrentSpan(); span.disableAutoEnd(); const res = original .apply(ctx, originalArgs) .then(data => { span.d = Date.now() - span.ts; span.transmitManual(); return data; }) .catch(err => { addErrorToSpan(err, span); span.d = Date.now() - span.ts; span.transmitManual(); }); return res; }); } else { return original.apply(ctx, originalArgs); } } function shimSQSConsumerReceiveMessage(original) { return function () { if (isActive) { const originalArgs = new Array(arguments.length); for (let i = 0; i < originalArgs.length; i++) { originalArgs[i] = arguments[i]; } return instrumentedSQSConsumerReceiveMessage(this, original, originalArgs); } return original.apply(this, arguments); }; } function instrumentedSQSConsumerReceiveMessage(ctx, original, originalArgs) { return cls.ns.runAndReturn(() => { // save the queue url to be used in executeHandler ctx._instanaSqsQueueUrl = originalArgs[0].QueueUrl; return original.apply(ctx, originalArgs); }); }