diff --git a/libraries/metrics/initialize.js b/libraries/metrics/initialize.js index 1028ee06c3..f1a77666c7 100644 --- a/libraries/metrics/initialize.js +++ b/libraries/metrics/initialize.js @@ -5,6 +5,8 @@ * before any other module to support code instrumentation. */ +const metricsModuleImportStartTime = performance.now() + const APP_NAME = process.env.METRICS_APP_NAME || 'unknown' const BUILD_VERSION = process.env.BUILD_VERSION const ENABLE_PROFILE_AGENT = process.env.ENABLE_PROFILE_AGENT === 'true' @@ -103,3 +105,5 @@ function recordProcessStart() { const metrics = require('.') metrics.inc('process_startup') } + +module.exports = { metricsModuleImportStartTime } diff --git a/services/web/app.mjs b/services/web/app.mjs index 5ece02cf32..1538e60149 100644 --- a/services/web/app.mjs +++ b/services/web/app.mjs @@ -1,5 +1,5 @@ // Metrics must be initialized before importing anything else -import '@overleaf/metrics/initialize.js' +import { metricsModuleImportStartTime } from '@overleaf/metrics/initialize.js' import Modules from './app/src/infrastructure/Modules.js' import metrics from '@overleaf/metrics' @@ -20,6 +20,13 @@ import FileWriter from './app/src/infrastructure/FileWriter.js' import { fileURLToPath } from 'node:url' import Features from './app/src/infrastructure/Features.js' +metrics.gauge( + 'web_startup', + performance.now() - metricsModuleImportStartTime, + 1, + { path: 'imports' } +) + logger.initialize(process.env.METRICS_APP_NAME || 'web') logger.logger.serializers.user = Serializers.user logger.logger.serializers.docs = Serializers.docs @@ -58,6 +65,29 @@ if ( ) } +// handle SIGTERM for graceful shutdown in kubernetes +process.on('SIGTERM', function (signal) { + triggerGracefulShutdown(Server.server, signal) +}) + +const beforeWaitForMongoAndGlobalBlobs = performance.now() +try { + await Promise.all([ + mongodb.connectionPromise, + mongoose.connectionPromise, + HistoryManager.promises.loadGlobalBlobs(), + ]) +} catch (err) { + logger.fatal({ err }, 'Cannot connect to mongo. Exiting.') + process.exit(1) +} +metrics.gauge( + 'web_startup', + performance.now() - beforeWaitForMongoAndGlobalBlobs, + 1, + { path: 'waitForMongoAndGlobalBlobs' } +) + const port = Settings.port || Settings.internal.web.port || 3000 const host = Settings.internal.web.host || '127.0.0.1' if (process.argv[1] === fileURLToPath(import.meta.url)) { @@ -69,42 +99,33 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { PlansLocator.ensurePlansAreSetupCorrectly() - Promise.all([ - mongodb.connectionPromise, - mongoose.connectionPromise, - HistoryManager.promises.loadGlobalBlobs(), - ]) - .then(async () => { - Server.server.listen(port, host, function () { - logger.debug(`web starting up, listening on ${host}:${port}`) - logger.debug(`${http.globalAgent.maxSockets} sockets enabled`) - // wait until the process is ready before monitoring the event loop - metrics.event_loop.monitor(logger) - }) - QueueWorkers.start() - await Modules.start() - }) - .catch(err => { - logger.fatal({ err }, 'Cannot connect to mongo. Exiting.') - process.exit(1) - }) + Server.server.listen(port, host, function () { + logger.debug(`web starting up, listening on ${host}:${port}`) + logger.debug(`${http.globalAgent.maxSockets} sockets enabled`) + // wait until the process is ready before monitoring the event loop + metrics.event_loop.monitor(logger) + + // Record metrics for the total startup time before listening on HTTP. + metrics.gauge( + 'web_startup', + performance.now() - metricsModuleImportStartTime, + 1, + { path: 'metricsModuleImportToHTTPListen' } + ) + }) + try { + QueueWorkers.start() + } catch (err) { + logger.fatal({ err }, 'failed to start queue processing') + } + try { + await Modules.start() + } catch (err) { + logger.fatal({ err }, 'failed to start web module background jobs') + } } // initialise site admin tasks -Promise.all([ - mongodb.connectionPromise, - mongoose.connectionPromise, - HistoryManager.promises.loadGlobalBlobs(), -]) - .then(() => SiteAdminHandler.initialise()) - .catch(err => { - logger.fatal({ err }, 'Cannot connect to mongo. Exiting.') - process.exit(1) - }) - -// handle SIGTERM for graceful shutdown in kubernetes -process.on('SIGTERM', function (signal) { - triggerGracefulShutdown(Server.server, signal) -}) +SiteAdminHandler.initialise() export default Server.server diff --git a/services/web/app/src/infrastructure/Modules.js b/services/web/app/src/infrastructure/Modules.js index a21be431c4..f746519612 100644 --- a/services/web/app/src/infrastructure/Modules.js +++ b/services/web/app/src/infrastructure/Modules.js @@ -4,6 +4,7 @@ const { promisify, callbackify } = require('util') const Settings = require('@overleaf/settings') const Views = require('./Views') const _ = require('lodash') +const Metrics = require('@overleaf/metrics') const MODULE_BASE_PATH = Path.join(__dirname, '/../../../modules') @@ -15,7 +16,11 @@ let _viewIncludes = {} async function modules() { if (!_modulesLoaded) { + const beforeLoadModules = performance.now() await loadModules() + Metrics.gauge('web_startup', performance.now() - beforeLoadModules, 1, { + path: 'loadModules', + }) } return _modules } diff --git a/services/web/app/src/infrastructure/Server.mjs b/services/web/app/src/infrastructure/Server.mjs index 3c7fd752d6..9e548bdc9e 100644 --- a/services/web/app/src/infrastructure/Server.mjs +++ b/services/web/app/src/infrastructure/Server.mjs @@ -372,6 +372,10 @@ if (Settings.enabledServices.includes('web')) { metrics.injectMetricsRoute(webRouter) metrics.injectMetricsRoute(privateApiRouter) +const beforeRouterInitialize = performance.now() await Router.initialize(webRouter, privateApiRouter, publicApiRouter) +metrics.gauge('web_startup', performance.now() - beforeRouterInitialize, 1, { + path: 'Router.initialize', +}) export default { app, server }