diff --git a/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/dynamodb.js b/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/dynamodb.js index 9c53743cb5..1ca9ab497a 100644 --- a/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/dynamodb.js +++ b/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/dynamodb.js @@ -95,6 +95,8 @@ class InstanaAWSDynamoDB extends InstanaAWSProduct { .catch(() => { /* silently ignore failed attempts to get the region */ }); + } else { + tracingUtil.handleUnexpectedReturnValue(regionPromise, span, this.spanName, 'config.region()'); } } } diff --git a/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/sqs-consumer.js b/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/sqs-consumer.js index cd29f1aba6..3e6daa8556 100644 --- a/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/sqs-consumer.js +++ b/packages/core/src/tracing/instrumentation/cloud/aws-sdk/v3/sqs-consumer.js @@ -24,17 +24,23 @@ function instrument(SQSConsumer) { span.disableAutoEnd(); const res = orig.apply(this, arguments); - res - .then(() => { - span.d = Date.now() - span.ts; - span.transmitManual(); - }) - .catch(err => { - span.ec = 1; - tracingUtil.setErrorDetails(span, err, 'sqs'); - span.d = Date.now() - span.ts; - span.transmitManual(); - }); + if (res && typeof res.then === 'function') { + res + .then(() => { + span.d = Date.now() - span.ts; + span.transmitManual(); + }) + .catch(err => { + span.ec = 1; + tracingUtil.setErrorDetails(span, err, 'sqs'); + span.d = Date.now() - span.ts; + span.transmitManual(); + }); + } else { + tracingUtil.handleUnexpectedReturnValue(res, span, 'sqs', 'consumer handler'); + span.d = Date.now() - span.ts; + span.transmitManual(); + } return res; }); @@ -56,17 +62,23 @@ function instrument(SQSConsumer) { span.disableAutoEnd(); const res = orig.apply(this, arguments); - res - .then(() => { - span.d = Date.now() - span.ts; - span.transmitManual(); - }) - .catch(err => { - span.ec = 1; - tracingUtil.setErrorDetails(span, err, 'sqs'); - span.d = Date.now() - span.ts; - span.transmitManual(); - }); + if (res && typeof res.then === 'function') { + res + .then(() => { + span.d = Date.now() - span.ts; + span.transmitManual(); + }) + .catch(err => { + span.ec = 1; + tracingUtil.setErrorDetails(span, err, 'sqs'); + span.d = Date.now() - span.ts; + span.transmitManual(); + }); + } else if (res !== undefined) { + tracingUtil.handleUnexpectedReturnValue(res, span, 'sqs', 'consumer batch handler'); + span.d = Date.now() - span.ts; + span.transmitManual(); + } return res; }); diff --git a/packages/core/src/tracing/instrumentation/cloud/azure/blob.js b/packages/core/src/tracing/instrumentation/cloud/azure/blob.js index d2b076cf13..72b84bc7f7 100644 --- a/packages/core/src/tracing/instrumentation/cloud/azure/blob.js +++ b/packages/core/src/tracing/instrumentation/cloud/azure/blob.js @@ -87,7 +87,8 @@ function instrumentingOperation({ op }; const promise = originalQuery.apply(ctx, argsForOriginalQuery); - if (promise && typeof promise.then === 'function') { + + if (promise && typeof promise?.then === 'function') { promise .then(value => { finishSpan(null, span); @@ -97,6 +98,9 @@ function instrumentingOperation({ finishSpan(error, span); return error; }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'azstorage', 'blob operation'); + finishSpan(null, span); } return promise; }); diff --git a/packages/core/src/tracing/instrumentation/cloud/gcp/pubsub.js b/packages/core/src/tracing/instrumentation/cloud/gcp/pubsub.js index a2adfec562..36e8472186 100644 --- a/packages/core/src/tracing/instrumentation/cloud/gcp/pubsub.js +++ b/packages/core/src/tracing/instrumentation/cloud/gcp/pubsub.js @@ -132,6 +132,11 @@ function instrumentedPublishMessage(ctx, originalPublishMessage, originalArgs) { throw err; } ); + } else if (!originalCallback) { + // If there's no callback and no promise, we need to finish the span + // This can happen in some edge cases + tracingUtil.handleUnexpectedReturnValue(thenable, span, 'gcps', 'publish message'); + finishSpan(null, null, span); } return thenable; }); diff --git a/packages/core/src/tracing/instrumentation/cloud/gcp/storage.js b/packages/core/src/tracing/instrumentation/cloud/gcp/storage.js index c1d4fbb43d..9635831bce 100644 --- a/packages/core/src/tracing/instrumentation/cloud/gcp/storage.js +++ b/packages/core/src/tracing/instrumentation/cloud/gcp/storage.js @@ -416,11 +416,14 @@ function instrumentedOperation(operation, extractorPre, extractorPost, ctx, orig } const promise = original.apply(ctx, originalArgs); - if (promise) { + if (promise && typeof promise.then === 'function') { promise.then( result => finishSpan(null, Array.isArray(result) ? result[0] : result, span, extractorPost), e => finishSpan(e, null, span, extractorPost) ); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'gcs', operation); + finishSpan(null, null, span, extractorPost); } return promise; }); diff --git a/packages/core/src/tracing/instrumentation/control_flow/graphqlSubscriptions.js b/packages/core/src/tracing/instrumentation/control_flow/graphqlSubscriptions.js index 8197259f91..99634c6511 100644 --- a/packages/core/src/tracing/instrumentation/control_flow/graphqlSubscriptions.js +++ b/packages/core/src/tracing/instrumentation/control_flow/graphqlSubscriptions.js @@ -8,6 +8,7 @@ const shimmer = require('../../shimmer'); const hook = require('../../../util/hook'); +const tracingUtil = require('../../tracingUtil'); const cls = require('../../cls'); let isActive = false; @@ -62,18 +63,29 @@ function shimPushValue(originalFunction) { function shimPullValue(originalFunction) { return function () { const pullPromise = originalFunction.apply(this, arguments); - return pullPromise.then(result => { - if (result && result.value && result.value[CLS_CONTEXT_SYMBOL]) { - const clsContext = result.value[CLS_CONTEXT_SYMBOL]; - if (isActive && clsContext) { - cls.ns.enter(clsContext); - setImmediate(() => { - cls.ns.exit(clsContext); - }); + + if (pullPromise && typeof pullPromise.then === 'function') { + return pullPromise.then(result => { + if (result && result.value && result.value[CLS_CONTEXT_SYMBOL]) { + const clsContext = result.value[CLS_CONTEXT_SYMBOL]; + if (isActive && clsContext) { + cls.ns.enter(clsContext); + setImmediate(() => { + cls.ns.exit(clsContext); + }); + } } + return result; + }); + } else { + // will the context change ? Maybe check and remove this case + const span = cls.getCurrentSpan(); + if (span) { + tracingUtil.handleUnexpectedReturnValue(pullPromise, span, 'graphql.subscription', 'pull value'); } - return result; - }); + } + + return pullPromise; }; } diff --git a/packages/core/src/tracing/instrumentation/databases/couchbase.js b/packages/core/src/tracing/instrumentation/databases/couchbase.js index 2d7a0df358..b3dfc9059f 100644 --- a/packages/core/src/tracing/instrumentation/databases/couchbase.js +++ b/packages/core/src/tracing/instrumentation/databases/couchbase.js @@ -77,7 +77,7 @@ function instrumentConnect(originalConnect) { const prom = originalConnect.apply(originalThis, originalArgs); - if (prom && prom.then) { + if (prom && typeof prom.then === 'function') { prom.then(cluster => { instrumentCluster(cluster, connectionStr); return cluster; @@ -426,7 +426,7 @@ function instrumentTransactions(cluster, connectionStr) { const result = originalFn.apply(this, arguments); - if (result.then && result.catch) { + if (result && result?.then && result?.catch) { result .then(() => { span.d = Date.now() - span.ts; @@ -486,9 +486,19 @@ function instrumentOperation({ connectionStr, bucketName, getBucketTypeFn, sql, const { originalCallback, callbackIndex } = tracingUtil.findCallback(originalArgs); if (callbackIndex < 0) { - const prom = original.apply(originalThis, originalArgs); + // Case 4: Handle synchronous validation errors for promise-based calls + let prom; + try { + prom = original.apply(originalThis, originalArgs); + } catch (syncError) { + span.ec = 1; + tracingUtil.setErrorDetails(span, syncError, 'couchbase'); + span.d = Date.now() - span.ts; + span.transmit(); + throw syncError; + } - if (prom.then && prom.catch) { + if (typeof prom?.then === 'function' && typeof prom?.catch === 'function') { prom .then(result => { if (resultHandler) { @@ -505,27 +515,42 @@ function instrumentOperation({ connectionStr, bucketName, getBucketTypeFn, sql, span.d = Date.now() - span.ts; span.transmit(); }); + } else if (prom !== undefined) { + // Case 5: Use utility function + tracingUtil.handleUnexpectedReturnValue(prom, span, 'couchbase', 'operation'); + + span.d = Date.now() - span.ts; + span.transmit(); } return prom; } else { - originalArgs[callbackIndex] = cls.ns.bind(function instanaCallback(err, result) { - if (err) { - span.ec = 1; - tracingUtil.setErrorDetails(span, err, 'couchbase'); - } + // Case 4: Handle synchronous validation errors for callback-based calls + try { + originalArgs[callbackIndex] = cls.ns.bind(function instanaCallback(err, result) { + if (err) { + span.ec = 1; + tracingUtil.setErrorDetails(span, err, 'couchbase'); + } - if (resultHandler) { - resultHandler(span, result); - } + if (resultHandler) { + resultHandler(span, result); + } - span.d = Date.now() - span.ts; - span.transmit(); + span.d = Date.now() - span.ts; + span.transmit(); - return originalCallback.apply(this, arguments); - }); + return originalCallback.apply(this, arguments); + }); - return original.apply(originalThis, originalArgs); + return original.apply(originalThis, originalArgs); + } catch (syncError) { + span.ec = 1; + tracingUtil.setErrorDetails(span, syncError, 'couchbase'); + span.d = Date.now() - span.ts; + span.transmit(); + throw syncError; + } } }); }; diff --git a/packages/core/src/tracing/instrumentation/databases/db2.js b/packages/core/src/tracing/instrumentation/databases/db2.js index c6819d219c..579e5577b5 100644 --- a/packages/core/src/tracing/instrumentation/databases/db2.js +++ b/packages/core/src/tracing/instrumentation/databases/db2.js @@ -314,6 +314,7 @@ function instrumentQueryHelper(ctx, originalArgs, originalFunction, stmt, isAsyn return originalFunction.apply(ctx, originalArgs); } + // Case 4: Handle synchronous validation errors for promise-based calls const resultPromise = originalFunction.apply(ctx, originalArgs); if (resultPromise && typeof resultPromise.then === 'function' && typeof resultPromise.catch === 'function') { @@ -328,9 +329,12 @@ function instrumentQueryHelper(ctx, originalArgs, originalFunction, stmt, isAsyn finishSpan(ctx, null, span); return err; }); - - return resultPromise; + } else { + tracingUtil.handleUnexpectedReturnValue(resultPromise, span, 'ibmdb2', 'query'); + finishSpan(ctx, null, span); } + + return resultPromise; }); } diff --git a/packages/core/src/tracing/instrumentation/databases/elasticsearch.js b/packages/core/src/tracing/instrumentation/databases/elasticsearch.js index 4877c53604..5d64f49db4 100644 --- a/packages/core/src/tracing/instrumentation/databases/elasticsearch.js +++ b/packages/core/src/tracing/instrumentation/databases/elasticsearch.js @@ -155,10 +155,17 @@ function instrumentApi(client, actionPath, clusterInfo) { } else { // eslint-disable-next-line no-useless-catch try { - return originalFunction.apply(ctx, originalArgs).then(onSuccess.bind(null, span), error => { - onError(span, error); - throw error; - }); + const promise = originalFunction.apply(ctx, originalArgs); + if (typeof promise?.then === 'function') { + return promise.then(onSuccess.bind(null, span), error => { + onError(span, error); + throw error; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'elasticsearch', `action "${action}"`); + onSuccess(span, {}); + } + return promise; } catch (e) { // Immediately cleanup on synchronous errors. throw e; @@ -448,10 +455,17 @@ function instrumentedRequest(ctx, origEsReq, originalArgs) { } else { // eslint-disable-next-line no-useless-catch try { - return origEsReq.apply(ctx, originalArgs).then(onSuccess.bind(null, span), error => { - onError(span, error); - throw error; - }); + const promise = origEsReq.apply(ctx, originalArgs); + if (typeof promise?.then === 'function') { + return promise.then(onSuccess.bind(null, span), error => { + onError(span, error); + throw error; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'elasticsearch', 'transport request'); + onSuccess(span, {}); + } + return promise; } catch (e) { // Immediately cleanup on synchronous errors. throw e; diff --git a/packages/core/src/tracing/instrumentation/databases/ioredis.js b/packages/core/src/tracing/instrumentation/databases/ioredis.js index e88a1c69e3..acc49ef698 100644 --- a/packages/core/src/tracing/instrumentation/databases/ioredis.js +++ b/packages/core/src/tracing/instrumentation/databases/ioredis.js @@ -97,11 +97,16 @@ function instrumentSendCommand(original) { span.stack = tracingUtil.getStackTrace(wrappedInternalSendCommand); callback = cls.ns.bind(onResult); - command.promise.then( - // make sure that the first parameter is never truthy - callback.bind(null, null), - callback - ); + if (typeof command.promise?.then === 'function') { + command.promise.then( + // make sure that the first parameter is never truthy + callback.bind(null, null), + callback + ); + } else { + tracingUtil.handleUnexpectedReturnValue(command.promise, span, 'redis', `command "${command.name}"`); + callback(null); + } return original.apply(client, argsForOriginal); @@ -190,7 +195,7 @@ function instrumentMultiOrPipelineExec(clsContextForMultiOrPipeline, commandName span.ts = Date.now(); const result = original.apply(this, arguments); - if (result.then) { + if (typeof result?.then === 'function') { result.then( results => { endCallback.call(null, clsContextForMultiOrPipeline, span, null, results); @@ -199,6 +204,9 @@ function instrumentMultiOrPipelineExec(clsContextForMultiOrPipeline, commandName endCallback.call(null, clsContextForMultiOrPipeline, span, error, []); } ); + } else if (result !== undefined) { + tracingUtil.handleUnexpectedReturnValue(result, span, 'redis', `${commandName} exec`); + endCallback.call(null, clsContextForMultiOrPipeline, span, null, []); } return result; }; diff --git a/packages/core/src/tracing/instrumentation/databases/mongodb.js b/packages/core/src/tracing/instrumentation/databases/mongodb.js index 7582e1917c..a2ecf1f663 100644 --- a/packages/core/src/tracing/instrumentation/databases/mongodb.js +++ b/packages/core/src/tracing/instrumentation/databases/mongodb.js @@ -457,7 +457,7 @@ function handleCallbackOrPromise(ctx, originalArgs, originalFunction, span) { const resultPromise = originalFunction.apply(ctx, originalArgs); - if (resultPromise && resultPromise.then) { + if (resultPromise && typeof resultPromise.then === 'function') { resultPromise .then(result => { span.d = Date.now() - span.ts; @@ -471,6 +471,11 @@ function handleCallbackOrPromise(ctx, originalArgs, originalFunction, span) { span.transmit(); return err; }); + } else { + tracingUtil.handleUnexpectedReturnValue(resultPromise, span, 'mongo', 'command'); + + span.d = Date.now() - span.ts; + span.transmit(); } return resultPromise; diff --git a/packages/core/src/tracing/instrumentation/databases/mssql.js b/packages/core/src/tracing/instrumentation/databases/mssql.js index d1cf837774..d01ee7a024 100644 --- a/packages/core/src/tracing/instrumentation/databases/mssql.js +++ b/packages/core/src/tracing/instrumentation/databases/mssql.js @@ -88,7 +88,8 @@ function instrumentedMethod(ctx, originalFunction, originalArgs, stackTraceRef, } const promise = originalFunction.apply(ctx, originalArgs); - if (typeof promise.then === 'function') { + + if (typeof promise?.then === 'function') { promise .then(value => { finishSpan(null, span); @@ -98,6 +99,9 @@ function instrumentedMethod(ctx, originalFunction, originalArgs, stackTraceRef, finishSpan(error, span); return error; }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'mssql', 'query/execute'); + finishSpan(null, span); } return promise; }); diff --git a/packages/core/src/tracing/instrumentation/databases/mysql.js b/packages/core/src/tracing/instrumentation/databases/mysql.js index a33688fa86..88f9237f74 100644 --- a/packages/core/src/tracing/instrumentation/databases/mysql.js +++ b/packages/core/src/tracing/instrumentation/databases/mysql.js @@ -182,20 +182,28 @@ function instrumentedAccessFunction( if (isPromiseImpl) { const resultPromise = originalFunction.apply(ctx, originalArgs); - resultPromise - .then(result => { - span.d = Date.now() - span.ts; - span.transmit(); - return result; - }) - .catch(error => { - span.ec = 1; - tracingUtil.setErrorDetails(span, error, exports.spanName); - - span.d = Date.now() - span.ts; - span.transmit(); - return error; - }); + if (typeof resultPromise?.then === 'function') { + resultPromise + .then(result => { + span.d = Date.now() - span.ts; + span.transmit(); + return result; + }) + .catch(error => { + span.ec = 1; + tracingUtil.setErrorDetails(span, error, exports.spanName); + + span.d = Date.now() - span.ts; + span.transmit(); + throw error; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(resultPromise, span, 'mysql', 'query/execute'); + + span.d = Date.now() - span.ts; + span.transmit(); + } + return resultPromise; } @@ -238,12 +246,15 @@ function shimGetConnection(original) { function shimPromiseConnection(original) { return function getConnection() { - return original.apply(this, arguments).then(connection => { - shimmer.wrap(connection, 'query', shimPromiseQuery); - shimmer.wrap(connection, 'execute', shimPromiseExecute); - - return connection; - }); + const promise = original.apply(this, arguments); + if (typeof promise?.then === 'function') { + return promise.then(connection => { + shimmer.wrap(connection, 'query', shimPromiseQuery); + shimmer.wrap(connection, 'execute', shimPromiseExecute); + + return connection; + }); + } }; } diff --git a/packages/core/src/tracing/instrumentation/databases/pg.js b/packages/core/src/tracing/instrumentation/databases/pg.js index eed210ad70..b299802775 100644 --- a/packages/core/src/tracing/instrumentation/databases/pg.js +++ b/packages/core/src/tracing/instrumentation/databases/pg.js @@ -85,6 +85,7 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { } const promise = originalQuery.apply(ctx, argsForOriginalQuery); + if (promise && typeof promise.then === 'function') { promise .then(value => { @@ -95,6 +96,9 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { finishSpan(error, span); return error; }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'pg', 'query'); + finishSpan(null, span); } return promise; }); diff --git a/packages/core/src/tracing/instrumentation/databases/prisma.js b/packages/core/src/tracing/instrumentation/databases/prisma.js index 126b2e6080..7344af34f6 100644 --- a/packages/core/src/tracing/instrumentation/databases/prisma.js +++ b/packages/core/src/tracing/instrumentation/databases/prisma.js @@ -166,11 +166,13 @@ function instrumentedRequest(ctx, originalRequest, argsForOriginalRequest) { provider: providerAndDataSourceUri.provider, url: providerAndDataSourceUri.dataSourceUrl }; + const requestPromise = originalRequest.apply(ctx, argsForOriginalRequest); + if (!requestPromise && typeof requestPromise.then !== 'function') { span.cancel(); return requestPromise; - } else { + } else if (typeof requestPromise?.then === 'function') { return requestPromise .then(value => { finishSpan(null, span); @@ -180,6 +182,10 @@ function instrumentedRequest(ctx, originalRequest, argsForOriginalRequest) { finishSpan(error, span); return error; }); + } else { + tracingUtil.handleUnexpectedReturnValue(requestPromise, span, 'prisma', '_request'); + finishSpan(null, span); + return requestPromise; } }); } diff --git a/packages/core/src/tracing/instrumentation/databases/redis.js b/packages/core/src/tracing/instrumentation/databases/redis.js index 4f88ec8c85..eb831d7c29 100644 --- a/packages/core/src/tracing/instrumentation/databases/redis.js +++ b/packages/core/src/tracing/instrumentation/databases/redis.js @@ -342,11 +342,10 @@ function instrumentCommand(original, command, address, cbStyle) { if (typeof userProvidedCallback !== 'function') { userProvidedCallback = null; modifiedArgs.push(callback); - return original.apply(origCtx, modifiedArgs); } else { modifiedArgs[modifiedArgs.length - 1] = callback; - return original.apply(origCtx, modifiedArgs); } + return original.apply(origCtx, modifiedArgs); } else { const promise = original.apply(origCtx, origArgs); if (typeof promise?.then === 'function') { @@ -360,7 +359,8 @@ function instrumentCommand(original, command, address, cbStyle) { return error; }); } else { - // UNKNOWN CASE + // Case 5: Unsupported/unknown case - use utility function + tracingUtil.handleUnexpectedReturnValue(promise, span, 'redis', `command "${command}"`); onResult(); } return promise; @@ -492,6 +492,10 @@ function instrumentMultiExec(origCtx, origArgs, original, address, isAtomic, cbS onResult(error); return error; }); + } else { + // Case 5: Unsupported/unknown case - use utility function + tracingUtil.handleUnexpectedReturnValue(promise, span, 'redis', 'multi/pipeline operation'); + onResult(); } return promise; diff --git a/packages/core/src/tracing/instrumentation/frameworks/koa.js b/packages/core/src/tracing/instrumentation/frameworks/koa.js index 48b72c57b4..e00886f6f6 100644 --- a/packages/core/src/tracing/instrumentation/frameworks/koa.js +++ b/packages/core/src/tracing/instrumentation/frameworks/koa.js @@ -8,6 +8,7 @@ const shimmer = require('../../shimmer'); const hook = require('../../../util/hook'); +const tracingUtil = require('../../tracingUtil'); const httpServer = require('../protocols/httpServer'); const cls = require('../../cls'); @@ -61,20 +62,28 @@ function instrumentedRoutes(thisContext, originalRoutes, originalArgs) { const instrumentedDispatch = function (ctx, next) { if (active && cls.isTracing()) { const dispatchResult = dispatch.apply(this, arguments); - return dispatchResult.then(resolvedValue => { - if (ctx.matched && ctx.matched.length && ctx.matched.length > 0) { - const matchedRouteLayers = ctx.matched.slice(); - matchedRouteLayers.sort(byLeastSpecificLayer); - const mostSpecificPath = normalizeLayerPath(matchedRouteLayers[matchedRouteLayers.length - 1].path); - annotateHttpEntrySpanWithPathTemplate(mostSpecificPath); + if (typeof dispatchResult?.then === 'function') { + return dispatchResult.then(resolvedValue => { + if (ctx.matched && ctx.matched.length && ctx.matched.length > 0) { + const matchedRouteLayers = ctx.matched.slice(); + matchedRouteLayers.sort(byLeastSpecificLayer); + const mostSpecificPath = normalizeLayerPath(matchedRouteLayers[matchedRouteLayers.length - 1].path); + annotateHttpEntrySpanWithPathTemplate(mostSpecificPath); + } + return resolvedValue; + }); + } else { + // context same ? check + const span = cls.getCurrentSpan(); + if (span) { + tracingUtil.handleUnexpectedReturnValue(dispatchResult, span, 'http', 'koa router dispatch'); } - return resolvedValue; - }); + } + return dispatchResult; } else { return dispatch.apply(this, arguments); } }; - // The router attaches itself as a property to the dispatch function and other methods in koa-router rely on this, so // we need to attach this property to our dispatch function, too. instrumentedDispatch.router = dispatch.router; diff --git a/packages/core/src/tracing/instrumentation/messaging/amqp.js b/packages/core/src/tracing/instrumentation/messaging/amqp.js index d0fcad6b46..cc0368f43a 100644 --- a/packages/core/src/tracing/instrumentation/messaging/amqp.js +++ b/packages/core/src/tracing/instrumentation/messaging/amqp.js @@ -249,56 +249,64 @@ function instrumentedChannelModelGet(ctx, originalGet, originalArgs) { kind: constants.ENTRY }); - return originalGet.apply(ctx, originalArgs).then(result => { - if (!result) { - // get did not fetch a new message from RabbitMQ (because the queue has no messages), no need to create a span. - span.cancel(); - return result; - } - const fields = result.fields || {}; - const headers = result.properties && result.properties.headers ? result.properties.headers : {}; - - if (tracingUtil.readAttribCaseInsensitive(headers, constants.traceLevelHeaderName) === '0') { - cls.setTracingLevel('0'); - span.cancel(); - return result; - } + const promise = originalGet.apply(ctx, originalArgs); + if (typeof promise?.then === 'function') { + return promise.then(result => { + if (!result) { + // get did not fetch a new message from RabbitMQ (because the queue has no messages), no need to create a + // span. + span.cancel(); + return result; + } + const fields = result.fields || {}; + const headers = result.properties && result.properties.headers ? result.properties.headers : {}; - const traceId = tracingUtil.readAttribCaseInsensitive(headers, constants.traceIdHeaderName); - const parentSpanId = tracingUtil.readAttribCaseInsensitive(headers, constants.spanIdHeaderName); - if (traceId && parentSpanId) { - span.t = traceId; - span.p = parentSpanId; - } + if (tracingUtil.readAttribCaseInsensitive(headers, constants.traceLevelHeaderName) === '0') { + cls.setTracingLevel('0'); + span.cancel(); + return result; + } - span.ts = Date.now(); - span.stack = tracingUtil.getStackTrace(instrumentedChannelModelGet); - span.data.rabbitmq = { - sort: 'consume' - }; + const traceId = tracingUtil.readAttribCaseInsensitive(headers, constants.traceIdHeaderName); + const parentSpanId = tracingUtil.readAttribCaseInsensitive(headers, constants.spanIdHeaderName); + if (traceId && parentSpanId) { + span.t = traceId; + span.p = parentSpanId; + } - if (ctx.connection.stream) { - span.data.rabbitmq.address = - typeof ctx.connection.stream.getProtocol === 'function' - ? 'amqps://' - : // - `amqp://${ctx.connection.stream.remoteAddress}:${ctx.connection.stream.remotePort}`; - } - if (fields.exchange) { - span.data.rabbitmq.exchange = fields.exchange; - } - if (fields.routingKey) { - span.data.rabbitmq.key = fields.routingKey; - } + span.ts = Date.now(); + span.stack = tracingUtil.getStackTrace(instrumentedChannelModelGet); + span.data.rabbitmq = { + sort: 'consume' + }; + + if (ctx.connection.stream) { + span.data.rabbitmq.address = + typeof ctx.connection.stream.getProtocol === 'function' + ? 'amqps://' + : // + `amqp://${ctx.connection.stream.remoteAddress}:${ctx.connection.stream.remotePort}`; + } + if (fields.exchange) { + span.data.rabbitmq.exchange = fields.exchange; + } + if (fields.routingKey) { + span.data.rabbitmq.key = fields.routingKey; + } - setImmediate(() => { - // Client code is expected to end the span manually, end it automatically in case client code doesn't. Child - // exit spans won't be captured, but at least the RabbitMQ entry span is there. - span.d = Date.now() - span.ts; - span.transmit(); + setImmediate(() => { + // Client code is expected to end the span manually, end it automatically in case client code doesn't. Child + // exit spans won't be captured, but at least the RabbitMQ entry span is there. + span.d = Date.now() - span.ts; + span.transmit(); + }); + return result; }); - return result; - }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'rabbitmq', 'channel.get'); + span.cancel(); + } + return promise; }); } diff --git a/packages/core/src/tracing/instrumentation/messaging/bull.js b/packages/core/src/tracing/instrumentation/messaging/bull.js index f09fc0474d..3b8b7f17a7 100644 --- a/packages/core/src/tracing/instrumentation/messaging/bull.js +++ b/packages/core/src/tracing/instrumentation/messaging/bull.js @@ -100,15 +100,21 @@ function instrumentedJobCreate(ctx, originalJobCreate, originalArgs, options) { const promise = originalJobCreate.apply(ctx, originalArgs); - return promise - .then(job => { - finishSpan(null, job, span); - return job; - }) - .catch(err => { - finishSpan(err, null, span); - return err; - }); + if (typeof promise?.then === 'function') { + return promise + .then(job => { + finishSpan(null, job, span); + return job; + }) + .catch(err => { + finishSpan(err, null, span); + return err; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'bull', 'job.create'); + finishSpan(null, null, span); + } + return promise; }); } @@ -258,20 +264,27 @@ function instrumentedProcessJob(ctx, originalProcessJob, originalArgs) { const promise = originalProcessJob.apply(ctx, originalArgs); - return promise - .then(data => { - finishSpan(job.failedReason, data, span); - // Make sure the instana foreigner data is removed. - delete options.X_INSTANA_L; - return data; - }) - .catch(err => { - addErrorToSpan(err, span); - finishSpan(null, null, span); - // Make sure the instana foreigner data is removed. - delete options.X_INSTANA_L; - throw err; - }); + if (promise && typeof promise.then === 'function') { + return promise + .then(data => { + finishSpan(job.failedReason, data, span); + // Make sure the instana foreigner data is removed. + delete options.X_INSTANA_L; + return data; + }) + .catch(err => { + addErrorToSpan(err, span); + finishSpan(null, null, span); + // Make sure the instana foreigner data is removed. + delete options.X_INSTANA_L; + throw err; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'bull', 'job.process'); + finishSpan(null, null, span); + delete options.X_INSTANA_L; + } + return promise; }); } diff --git a/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js b/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js index f887225f13..51faafac2a 100644 --- a/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js +++ b/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js @@ -96,20 +96,27 @@ function instrumentedSend(ctx, originalSend, originalArgs, topic, messages) { } span.stack = tracingUtil.getStackTrace(instrumentedSend); - return originalSend - .apply(ctx, originalArgs) - .then(result => { - span.d = Date.now() - span.ts; - span.transmit(); - return result; - }) - .catch(error => { - span.ec = 1; - tracingUtil.setErrorDetails(span, error, 'kafka'); - span.d = Date.now() - span.ts; - span.transmit(); - throw error; - }); + const promise = originalSend.apply(ctx, originalArgs); + if (typeof promise?.then === 'function') { + return promise + .then(result => { + span.d = Date.now() - span.ts; + span.transmit(); + return result; + }) + .catch(error => { + span.ec = 1; + tracingUtil.setErrorDetails(span, error, 'kafka'); + span.d = Date.now() - span.ts; + span.transmit(); + throw error; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'kafka', 'producer.send'); + span.d = Date.now() - span.ts; + span.transmit(); + } + return promise; }); } @@ -175,20 +182,27 @@ function instrumentedSendBatch(ctx, originalSendBatch, originalArgs, topicMessag span.b = { s: messageCount }; } - return originalSendBatch - .apply(ctx, originalArgs) - .then(result => { - span.d = Date.now() - span.ts; - span.transmit(); - return result; - }) - .catch(error => { - span.ec = 1; - tracingUtil.setErrorDetails(span, error, 'kafka'); - span.d = Date.now() - span.ts; - span.transmit(); - throw error; - }); + const promise = originalSendBatch.apply(ctx, originalArgs); + if (typeof promise?.then === 'function') { + return promise + .then(result => { + span.d = Date.now() - span.ts; + span.transmit(); + return result; + }) + .catch(error => { + span.ec = 1; + tracingUtil.setErrorDetails(span, error, 'kafka'); + span.d = Date.now() - span.ts; + span.transmit(); + throw error; + }); + } else { + tracingUtil.handleUnexpectedReturnValue(promise, span, 'kafka', 'producer.sendBatch'); + span.d = Date.now() - span.ts; + span.transmit(); + } + return promise; }); } diff --git a/packages/core/src/tracing/instrumentation/protocols/graphql.js b/packages/core/src/tracing/instrumentation/protocols/graphql.js index 5fcbcef99c..9f421e79dc 100644 --- a/packages/core/src/tracing/instrumentation/protocols/graphql.js +++ b/packages/core/src/tracing/instrumentation/protocols/graphql.js @@ -349,6 +349,15 @@ function shimApolloGatewayExecuteQueryPlanFunction(originalFunction) { throw err; } ); + } else { + tracingUtil.handleUnexpectedReturnValue( + resultPromise, + activeEntrySpan, + 'graphql.execute', + 'Apollo Gateway query plan' + ); + delete activeEntrySpan.postponeTransmitApolloGateway; + finishSpan(activeEntrySpan, {}); } return resultPromise; }; diff --git a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js index 9b72e210f6..74ccfcefdf 100644 --- a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js +++ b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js @@ -171,29 +171,35 @@ function instrument() { injectTraceCorrelationHeaders(originalArgs, span, w3cTraceContext); const fetchPromise = originalFetch.apply(originalThis, originalArgs); - fetchPromise - .then(response => { - span.data.http.status = response.status; - span.ec = response.status >= 500 ? 1 : 0; - capturedHeaders = mergeExtraHeadersFromFetchHeaders( - capturedHeaders, - response.headers, - extraHttpHeadersToCapture - ); - - span.d = Date.now() - span.ts; - if (capturedHeaders != null && Object.keys(capturedHeaders).length > 0) { - span.data.http.header = capturedHeaders; - } - span.transmit(); - }) - .catch(err => { - span.ec = 1; - tracingUtil.setErrorDetails(span, err, 'http'); - - span.d = Date.now() - span.ts; - span.transmit(); - }); + if (typeof fetchPromise?.then === 'function') { + fetchPromise + .then(response => { + span.data.http.status = response.status; + span.ec = response.status >= 500 ? 1 : 0; + capturedHeaders = mergeExtraHeadersFromFetchHeaders( + capturedHeaders, + response.headers, + extraHttpHeadersToCapture + ); + + span.d = Date.now() - span.ts; + if (capturedHeaders != null && Object.keys(capturedHeaders).length > 0) { + span.data.http.header = capturedHeaders; + } + span.transmit(); + }) + .catch(err => { + span.ec = 1; + tracingUtil.setErrorDetails(span, err, 'http'); + + span.d = Date.now() - span.ts; + span.transmit(); + }); + } else { + tracingUtil.handleUnexpectedReturnValue(fetchPromise, span, 'http', 'fetch'); + span.d = Date.now() - span.ts; + span.transmit(); + } return fetchPromise; }); diff --git a/packages/core/src/tracing/tracingUtil.js b/packages/core/src/tracing/tracingUtil.js index 17cd2684c3..1c9565444f 100644 --- a/packages/core/src/tracing/tracingUtil.js +++ b/packages/core/src/tracing/tracingUtil.js @@ -410,4 +410,47 @@ exports.setErrorDetails = function setErrorDetails(span, error, technology) { } catch (err) { logger.error('Failed to set error details on span:', err); } + + /** + * Handles unexpected return values from instrumented functions + * Logs a debug message and marks the span as incomplete when the return value is unsupported + * + * @param {*} returnValue - The return value from the instrumented function + * @param {import('../core').InstanaBaseSpan} targetSpan - The span to mark as incomplete + * @param {string} spanName - The name of the span (e.g., 'redis', 'postgres', 'mysql') + * @param {string} operationContext - Additional context about the operation (e.g., 'query', 'command') + * @returns {boolean} - Returns true if the return value was unexpected (not a promise), false otherwise + */ + exports.handleUnexpectedReturnValue = function handleUnexpectedReturnValue( + returnValue, + targetSpan, + spanName, + operationContext + ) { + if (typeof returnValue?.then === 'function') { + return false; + } + + // Case: This is the unexpected case where returnValue is not a promise + logger.debug( + `${spanName} instrumentation: Unexpected return value from ${operationContext}. ` + + `Expected a promise but got: ${typeof returnValue}. ` + + 'This may indicate an instrumentation bug or unsupported library behavior.' + ); + + // using sdk custom tags, we mark this span as incomplete + if (!targetSpan.data.sdk) { + targetSpan.data.sdk = {}; + } + if (!targetSpan.data.sdk.custom) { + targetSpan.data.sdk.custom = {}; + } + if (!targetSpan.data.sdk.custom.tags) { + targetSpan.data.sdk.custom.tags = {}; + } + targetSpan.data.sdk.custom.tags.incomplete = true; + targetSpan.data.sdk.custom.tags.incompleteReason = 'unexpected_return_type'; + + return true; + }; };