diff --git a/ghost/core/core/boot.js b/ghost/core/core/boot.js index 1b86330851..8d72dc6df3 100644 --- a/ghost/core/core/boot.js +++ b/ghost/core/core/boot.js @@ -483,7 +483,7 @@ async function bootGhost({backend = true, frontend = true, server = true} = {}) // Sentry must be initialized early, but requires config debug('Begin: Load sentry'); - require('./shared/sentry'); + const sentry = require('./shared/sentry'); debug('End: Load sentry'); // Step 2 - Start server with minimal app in global maintenance mode @@ -502,6 +502,9 @@ async function bootGhost({backend = true, frontend = true, server = true} = {}) debug('Begin: Get DB ready'); await initDatabase({config}); bootLogger.log('database ready'); + sentry.initQueryTracing( + require('./server/data/db/connection') + ); debug('End: Get DB ready'); // Step 4 - Load Ghost with all its services diff --git a/ghost/core/core/shared/SentryKnexTracingIntegration.js b/ghost/core/core/shared/SentryKnexTracingIntegration.js new file mode 100644 index 0000000000..a5dfc807a1 --- /dev/null +++ b/ghost/core/core/shared/SentryKnexTracingIntegration.js @@ -0,0 +1,67 @@ +/** + * @typedef {import('knex').Knex.Client} KnexClient + */ + +/** + * @typedef {import('@sentry/types').Integration} SentryIntegration + */ + +/** + * Sentry Knex tracing integration + * + * @implements {SentryIntegration} + */ +class SentryKnexTracingIntegration { + static id = 'Knex'; + + name = SentryKnexTracingIntegration.id; + + /** @type {KnexClient} */ + #knex; + + /** @type {Map} */ + #spanCache = new Map(); + + /** + * @param {KnexClient} knex + */ + constructor(knex) { + this.#knex = knex; + } + + /** + * @param {Function} addGlobalEventProcessor + * @param {Function} getCurrentHub + */ + setupOnce(addGlobalEventProcessor, getCurrentHub) { + this.#knex.on('query', (query) => { + const scope = getCurrentHub().getScope(); + const parentSpan = scope?.getSpan(); + + const span = parentSpan?.startChild({ + op: 'db.query', + description: query.sql + }); + + if (span) { + this.#spanCache.set(query.__knexQueryUid, span); + } + }); + + const handleQueryExecuted = (err, query) => { + const queryId = query.__knexQueryUid; + const span = this.#spanCache.get(queryId); + + if (span) { + span.finish(); + + this.#spanCache.delete(queryId); + } + }; + + this.#knex.on('query-response', handleQueryExecuted); + this.#knex.on('query-error', handleQueryExecuted); + } +} + +module.exports = SentryKnexTracingIntegration; diff --git a/ghost/core/core/shared/sentry.js b/ghost/core/core/shared/sentry.js index 139e7b1897..621a1a270f 100644 --- a/ghost/core/core/shared/sentry.js +++ b/ghost/core/core/shared/sentry.js @@ -1,4 +1,5 @@ const config = require('./config'); +const SentryKnexTracingIntegration = require('./SentryKnexTracingIntegration'); const sentryConfig = config.get('sentry'); const errors = require('@tryghost/errors'); @@ -93,7 +94,14 @@ if (sentryConfig && !sentryConfig.disabled) { }), tracingHandler: Sentry.Handlers.tracingHandler(), captureException: Sentry.captureException, - beforeSend: beforeSend + beforeSend: beforeSend, + initQueryTracing: (knex) => { + if (sentryConfig.tracing?.enabled === true) { + const integration = new SentryKnexTracingIntegration(knex); + + Sentry.addIntegration(integration); + } + } }; } else { const expressNoop = function (req, res, next) { @@ -106,6 +114,7 @@ if (sentryConfig && !sentryConfig.disabled) { requestHandler: expressNoop, errorHandler: expressNoop, tracingHandler: expressNoop, - captureException: noop + captureException: noop, + initQueryTracing: noop }; }