Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mongoose tracing support added to MongoDB #3252

Merged
merged 3 commits into from Feb 17, 2021

Conversation

underscorebrody
Copy link
Contributor

After this PR, Mongo tracing for projects using Mongoose should function as expected by initializing Sentry with the following:

Sentry.init({
  dsn: <DSN>,
  integrations: [new Tracing.Integrations.Mongo({useMongoose: true})],
  tracesSampleRate: 1.0,
})

This works because the Collection methods from mongodb are applied to NativeCollection on mongoose (which can be accessed at mongoose.Collection) See: https://github.com/Automattic/mongoose/blob/master/lib/drivers/node-mongodb-native/collection.js#L15-L22

Fixes #3176

@kamilogorek kamilogorek merged commit 61572bc into getsentry:master Feb 17, 2021
@kamilogorek
Copy link
Contributor

Awesome, thanks! :)

ahmedetefy pushed a commit that referenced this pull request Mar 10, 2021
* Add flag to switch to mongoose as the wrapped mongo library
This was referenced Mar 15, 2021
@derekrprice
Copy link

I tried this with @sentry/node and @sentry/tracing 6.3.0 with serverless functions and still am not seeing Mongo traces in my transactions. Is this feature released?

@kamilogorek
Copy link
Contributor

@derekrprice can you provide an example of how you instrument your serverless functions? Basically how you instantiate a trace and attach spans in your pipeline. (I've seen your support discussion, but I think it'll be quicker to discuss it here)

@derekrprice
Copy link

derekrprice commented May 18, 2021

Here is my entire instrumentation. We use this module as a higher order function (HOF) to wrap several different serverless functions, like so:

module.exports = withSentry(processLogFile);

Here is the actual implementation of withSentry:

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);
    }

    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();
    }
};

@derekrprice
Copy link

Nevermind, I've solved this. I had to move the scope.setSpan(transaction); up to just after I started the transaction rather than only setting it when capturing exceptions. Thanks!

@derekrprice
Copy link

I am still seeing a very odd issue. All my Mongo calls are showing up in Sentry as children of the lowest level function in the tree. In the following screenshots, most of these queries (all the inserts) are made by the *_timeseries process (the "Parent" in the shots). The *_sessions process only runs a few aggregate queries and the *_sessions span ID shouldn't even be available yet at the time that the insert queries are running in *_timeseries. Is this a problem with Sentry's server side processing?
image
image

@derekrprice
Copy link

Sentry.configureScope() appears to be a "global" call. Since dozens of serverless function instances are running in parallel on the same node, does that mean that the last one to run sets scope for all the others?

@kamilogorek
Copy link
Contributor

It is global, unless used inside domain instance, which we do in places where we expect scope separation. Which configureScope call do you have in mind exactly?

@derekrprice
Copy link

This is the new version of my withSentry module. You can see Sentry.configureScope() is called just after creating the transaction.

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(
                    "theFullContext",
                    Object.assign(
                        {},
                        ...Object.keys(context).map(k => ({
                            [k]: JSON.stringify(context[k])
                        }))
                    )
                );
            });
            await Sentry.flush();
        }

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

Every new serverless function call should have its own domain, if I understand your terminology correctly, but I didn't see anything about that in the docs. How would I configure that?

@derekrprice
Copy link

A PR doesn't seem like the right place to continue this discussion, so I opened bug report #3572

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Performance monitoring: Get Spans from Mongoose queries
3 participants