Skip to content

Serverless Function Tracing Needs an Isolated Scope #3572

Closed
@derekrprice

Description

@derekrprice

Package + Version

  • @sentry/node 6.3.6

Version:

6.3.6

Description

Here is my withSentry wrapper for Azure serverless Function invocations:

const Sentry = require("@sentry/node");
const Tracing = require("@sentry/tracing");
const mongodb = require("mongodb");

const config = require("../../config/sentry");

if (config.dsn) {
    Sentry.init({
        ...config,
        integrations: [
            // enable DB calls tracing
            new Tracing.Integrations.Mongo({ useMongoose: true })
            // new Tracing.Integrations.Mysql()  // No integration available for mysql2.
        ]
    });
}

/**
 * Add Sentry transaction information to outgoing messages.
 * @param {*} transaction
 * @param {*} results
 * @returns
 */
const propagateTransaction = (transaction, results) => {
    if (results && transaction) {
        for (const message of Object.values(results)) {
            Object.assign(message, {
                sentryParentSpanId: transaction.spanId,
                sentryTraceId: transaction.traceId,
                sentrySampled: transaction.sampled
            });
        }
    }
    return results;
};

/**
 * A higher order function which logs error data to Sentry.
 *
 * @param {*} callback          The actual function which might throw errors.
 */
module.exports = callback => async (context, trigger, ...args) => {
    let transaction;
    if (config.dsn) {
        // Define a transaction for performance tracking.
        const transactionContext = {
            op: "serverless-task",
            name: context?.executionContext?.functionName || "test-function"
        };
        if ("sentryParentSpanId" in trigger) {
            Object.assign(transactionContext, {
                parentSpanId: trigger.sentryParentSpanId,
                traceId: trigger.sentryTraceId,
                sampled: trigger.sentrySampled
            });
        }
        transaction = Sentry.startTransaction(transactionContext);

        Sentry.configureScope(scope => {
            scope.setSpan(transaction);
            scope.setContext(
                "executionContext",
                context?.executionContext || {
                    errorMessage:
                        "No executionContext defined.  This could be a notification from a test environment."
                }
            );
            scope.setContext("functionTrigger", trigger);
        });
    }

    try {
        return propagateTransaction(
            transaction,
            await callback(context, trigger, ...args)
        );
    } catch (error) {
        if (config.dsn) {
            Sentry.captureException(error, scope => {
                scope.setContext(
                    "executionContext",
                    context?.executionContext || {
                        errorMessage:
                            "No executionContext defined.  This could be a notification from a test environment."
                    }
                );
                scope.setContext("functionTrigger", trigger);
                scope.setContext(
                    "theFullContext",
                    Object.assign(
                        {},
                        ...Object.keys(context).map(k => ({
                            [k]: JSON.stringify(context[k])
                        }))
                    )
                );
            });
            scope.setSpan(transaction);
            await Sentry.flush();
        }

        throw error;
    } finally {
        await transaction?.finish();
    }
};

Defining an Azure Function like this:

module.exports = withSentry(processLogFile);

This logs everything correctly for exceptions since the scope is set immediately before sending the log, but performance tracing associates DB children with the wrong span and other context. This appears to be because the DB integrations use the span set on the scope. Since we have a high volume application (on the order of 3400 invocations per minute), many function invocations run concurrently and, since Sentry.configureScope() configures the span in a global scope, logged child DB transactions are associated not with the invocation they actually ran during but instead with context of the function invocation that last called Sentry.configureScope(). In these images, there are dozens of MongoDB calls associated with the *_sessions function even though the inserts are actually all called by the parent *_timeseries function:
image
image

Metadata

Metadata

Assignees

No one assigned

    Labels

    Package: nodeIssues related to the Sentry Node SDKPackage: serverlessIssues related to the Sentry Serverless SDK

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions