diff --git a/api/package-lock.json b/api/package-lock.json index 030f9d7bc06..638f6aed97c 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -29,6 +29,7 @@ "axios": "^1.0.0", "bcrypt": "^5.0.1", "cron-parser": "^4.9.0", + "datadog-metrics": "^0.12.0", "dayjs": "^1.11.5", "debug": "^4.3.4", "dotenv": "^16.0.1", @@ -1586,6 +1587,32 @@ "node": ">=6.9.0" } }, + "node_modules/@datadog/datadog-api-client": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@datadog/datadog-api-client/-/datadog-api-client-1.30.0.tgz", + "integrity": "sha512-Xa/7rpnM+QwL38+BiA7J9GUOA/ikQK4AhNMkqAGj0mLuyrZQv9oAlqxXuP7ZbdrdhB76h8ZX0Js13xEyvj/NaA==", + "license": "Apache-2.0", + "dependencies": { + "@types/buffer-from": "^1.1.0", + "@types/node": "*", + "@types/pako": "^1.0.3", + "buffer-from": "^1.1.2", + "cross-fetch": "^3.1.5", + "es6-promise": "^4.2.8", + "form-data": "^4.0.0", + "loglevel": "^1.8.1", + "pako": "^2.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@datadog/datadog-api-client/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/@eslint-community/eslint-plugin-eslint-comments": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.3.0.tgz", @@ -3624,6 +3651,15 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@types/buffer-from": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/buffer-from/-/buffer-from-1.1.3.tgz", + "integrity": "sha512-2lq4YC9uLUMGHkl2IDtX4tCXSo2+hwMpOJcY1qiIk1kybc31rIlPyM1HCVJhkPFIo75a/pOVxqyvwuf5TpCG/w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3662,6 +3698,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz", + "integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -3669,6 +3714,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pako": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", + "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4364,6 +4415,12 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -5032,6 +5089,15 @@ "node": ">=12.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5068,6 +5134,19 @@ "node": ">=0.10" } }, + "node_modules/datadog-metrics": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/datadog-metrics/-/datadog-metrics-0.12.0.tgz", + "integrity": "sha512-wxNg45gZr8+37yM8AjkfqUB6LCCRy2c0Ar0eI5vnGObC9ACJYMSr4fng7j5keUiiylD3jJIEAYVZTEr4zJ0DgA==", + "license": "MIT", + "dependencies": { + "@datadog/datadog-api-client": "^1.17.0", + "debug": "^4.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -5580,6 +5659,12 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -9122,6 +9207,19 @@ "node": ">=8" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loupe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", @@ -12951,6 +13049,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", diff --git a/api/package.json b/api/package.json index 94020ff063a..a62472e209e 100644 --- a/api/package.json +++ b/api/package.json @@ -36,6 +36,7 @@ "bcrypt": "^5.0.1", "cron-parser": "^4.9.0", "dayjs": "^1.11.5", + "datadog-metrics": "^0.12.0", "debug": "^4.3.4", "dotenv": "^16.0.1", "fast-levenshtein": "^3.0.0", diff --git a/api/sample.env b/api/sample.env index bf5178cceab..43ecf8b0824 100644 --- a/api/sample.env +++ b/api/sample.env @@ -40,6 +40,14 @@ REDIS_URL=redis://localhost:6379 # default: none # sample (everyday at 06:30 UTC): CACHE_RELOAD_TIME=30 6 * * * +# ===== +# DATADOG +# ===== + +# Datadog access context +# +DATADOG_API_KEY=nokey + # ========= # DATABASES # ========= diff --git a/api/server.js b/api/server.js index bb8d42051f3..7f49d9f92a2 100644 --- a/api/server.js +++ b/api/server.js @@ -1,5 +1,6 @@ import Oppsy from '@1024pix/oppsy'; import Hapi from '@hapi/hapi'; +import metrics from 'datadog-metrics'; import { parse } from 'neoqs'; import { setupErrorHandling } from './config/server-setup-error-handling.js'; @@ -35,12 +36,15 @@ import { config } from './src/shared/config.js'; import { monitoringTools } from './src/shared/infrastructure/monitoring-tools.js'; import { plugins } from './src/shared/infrastructure/plugins/index.js'; import { deserializer } from './src/shared/infrastructure/serializers/jsonapi/deserializer.js'; +import { logger } from './src/shared/infrastructure/utils/logger.js'; // bounded context migration import { sharedRoutes } from './src/shared/routes.js'; import { swaggers } from './src/shared/swaggers.js'; import { handleFailAction } from './src/shared/validate.js'; import { teamRoutes } from './src/team/application/routes.js'; +metrics.init({ host: 'myhost', prefix: 'myapp.' }); + const certificationRoutes = [ attachTargetProfileRoutes, certificationConfigurationRoutes, @@ -71,6 +75,21 @@ const createServer = async () => { if (logOpsMetrics) await enableOpsMetrics(server); + // initialisation of Datadog link for metrics publication + if (config.environment !== 'development' || config.featureToggles.isDirectMetricsEnabled) { + logger.info('Metric initialisation : linked to Datadog'); + metrics.init({ + host: config.infra.containerName, + prefix: 'tests.', + defaultTags: [`service:${config.infra.appName}`], + }); + } else { + logger.info('Metric initialisation : no reporter => no metrics sent'); + metrics.init({ reporter: metrics.NullReporter() }); + } + + if (logOpsMetrics) await enableOpsMetrics(server); + setupErrorHandling(server); setupAuthentication(server); @@ -125,23 +144,60 @@ const createBareServer = function () { }; const enableOpsMetrics = async function (server) { - const oppsy = new Oppsy(server); - - oppsy.on('ops', (data) => { - const knexPool = knex.client.pool; - server.log(['ops'], { - ...data, - knexPool: { - used: knexPool.numUsed(), - free: knexPool.numFree(), - pendingAcquires: knexPool.numPendingAcquires(), - pendingCreates: knexPool.numPendingCreates(), - }, + function collectMemoryStats() { + const memUsage = process.memoryUsage(); + metrics.gauge(`memory.rss`, memUsage.rss); + metrics.gauge('memory.heapTotal', memUsage.heapTotal); + metrics.gauge('memory.heapUsed', memUsage.heapUsed); + } + + setInterval(collectMemoryStats, 5000); + + const gaugeConnections = (pool) => () => { + metrics.gauge('db_connections_used', pool.numUsed()); + metrics.gauge('db_connections_free', pool.numFree()); + metrics.gauge('db_connections_pending_creation', pool.numPendingCreates()); + metrics.gauge('db_connections_pending_detroy', pool['pendingDestroys'].length); + }; + + const client = knex.client; + + client.pool.on('createSuccess', gaugeConnections(client.pool)); + client.pool.on('acquireSuccess', gaugeConnections(client.pool)); + client.pool.on('release', gaugeConnections(client.pool)); + client.pool.on('destroySuccess', gaugeConnections(client.pool)); + + if (config.featureToggles.isDirectMetricsEnabled) { + server.events.on('response', (request) => { + const info = request.info; + + const statusCode = request.raw.res.statusCode; + const responseTime = (info.completed !== undefined ? info.completed : info.responded) - info.received; + + metrics.histogram(`responseTime.${request.route}.${statusCode}`, responseTime); + metrics.increment(`api-calls.${request.route}.${statusCode}`); }); - }); + } - oppsy.start(logging.opsEventIntervalInSeconds * 1000); - server.oppsy = oppsy; + if (!config.featureToggles.isOppsyDisabled) { + const oppsy = new Oppsy(server); + + oppsy.on('ops', (data) => { + const knexPool = knex.client.pool; + server.log(['ops'], { + ...data, + knexPool: { + used: knexPool.numUsed(), + free: knexPool.numFree(), + pendingAcquires: knexPool.numPendingAcquires(), + pendingCreates: knexPool.numPendingCreates(), + }, + }); + }); + + oppsy.start(logging.opsEventIntervalInSeconds * 1000); + server.oppsy = oppsy; + } }; const setupDeserialization = function (server) { diff --git a/api/src/shared/config.js b/api/src/shared/config.js index 8381d8c88c0..720b1fbaba9 100644 --- a/api/src/shared/config.js +++ b/api/src/shared/config.js @@ -204,10 +204,12 @@ const configuration = (function () { ), isAsyncQuestRewardingCalculationEnabled: toBoolean(process.env.FT_ENABLE_ASYNC_QUESTS_REWARDS_CALCULATION), isCertificationTokenScopeEnabled: toBoolean(process.env.FT_ENABLE_CERTIF_TOKEN_SCOPE), + isDirectMetricsEnabled: toBoolean(process.env.FT_ENABLE_DIRECT_METRICS), isNeedToAdjustCertificationAccessibilityEnabled: toBoolean( process.env.FT_ENABLE_NEED_TO_ADJUST_CERTIFICATION_ACCESSIBILITY, ), isNewAuthenticationDesignEnabled: toBoolean(process.env.FT_NEW_AUTHENTICATION_DESIGN_ENABLED), + isOppsyDisabled: toBoolean(process.env.FT_OPPSY_DISABLED), isPix1dEnabled: toBoolean(process.env.FT_PIX_1D_ENABLED), isPixCompanionEnabled: toBoolean(process.env.FT_PIX_COMPANION_ENABLED), isSelfAccountDeletionEnabled: toBoolean(process.env.FT_SELF_ACCOUNT_DELETION), @@ -223,6 +225,8 @@ const configuration = (function () { enableRequestMonitoring: toBoolean(process.env.ENABLE_REQUEST_MONITORING), }, infra: { + appName: process.env.APP, + containerName: process.env.CONTAINER, concurrencyForHeavyOperations: _getNumber(process.env.INFRA_CONCURRENCY_HEAVY_OPERATIONS, 2), chunkSizeForCampaignResultProcessing: _getNumber(process.env.INFRA_CHUNK_SIZE_CAMPAIGN_RESULT_PROCESSING, 10), chunkSizeForOrganizationLearnerDataProcessing: _getNumber( @@ -290,6 +294,9 @@ const configuration = (function () { }, }, }, + metrics: { + flushIntervalSeconds: _getNumber(process.env.DIRECT_METRICS_FLUSH_INTERVAL, 5), + }, partner: { fetchTimeOut: ms(process.env.FETCH_TIMEOUT_MILLISECONDS || '20s'), }, @@ -412,7 +419,9 @@ const configuration = (function () { config.featureToggles.deprecatePoleEmploiPushNotification = false; config.featureToggles.isAlwaysOkValidateNextChallengeEndpointEnabled = false; config.featureToggles.isCertificationTokenScopeEnabled = false; + config.featureToggles.isDirectMetricsEnabled = false; config.featureToggles.isNeedToAdjustCertificationAccessibilityEnabled = false; + config.featureToggles.isOppsyDisabled = false; config.featureToggles.isPix1dEnabled = true; config.featureToggles.isPixCompanionEnabled = false; config.featureToggles.isSelfAccountDeletionEnabled = false; diff --git a/api/src/shared/infrastructure/plugins/pino.js b/api/src/shared/infrastructure/plugins/pino.js index ab2228fa88b..5431f44bf96 100644 --- a/api/src/shared/infrastructure/plugins/pino.js +++ b/api/src/shared/infrastructure/plugins/pino.js @@ -71,15 +71,20 @@ const plugin = { server.events.on('response', (request) => { const info = request.info; - logger.info( - { - queryParams: request.query, - req: request, - res: request.raw.res, - responseTime: (info.completed !== undefined ? info.completed : info.responded) - info.received, - }, - 'request completed', - ); + + const shouldLog = !config.featureToggles.isDirectMetricsEnabled; + + if (shouldLog || request.raw.res.statusCode != 200) { + logger.info( + { + queryParams: request.query, + req: request, + res: request.raw.res, + responseTime: (info.completed !== undefined ? info.completed : info.responded) - info.received, + }, + 'request completed', + ); + } }); }, }; diff --git a/api/tests/shared/acceptance/application/feature-toggles/feature-toggle-controller_test.js b/api/tests/shared/acceptance/application/feature-toggles/feature-toggle-controller_test.js index c7f3d8d50a7..723eae6241b 100644 --- a/api/tests/shared/acceptance/application/feature-toggles/feature-toggle-controller_test.js +++ b/api/tests/shared/acceptance/application/feature-toggles/feature-toggle-controller_test.js @@ -24,8 +24,10 @@ describe('Acceptance | Shared | Application | Controller | feature-toggle', func 'is-always-ok-validate-next-challenge-endpoint-enabled': false, 'is-async-quest-rewarding-calculation-enabled': false, 'is-certification-token-scope-enabled': false, + 'is-direct-metrics-enabled': false, 'is-need-to-adjust-certification-accessibility-enabled': false, 'is-new-authentication-design-enabled': false, + 'is-oppsy-disabled': false, 'is-pix1d-enabled': true, 'is-pix-companion-enabled': false, 'is-quest-enabled': false, diff --git a/package-lock.json b/package-lock.json index 08c1cd74b56..6292f3f7d5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { + "datadog-metrics": "^0.11.4", "npm-run-all2": "^6.0.0" }, "devDependencies": { @@ -176,6 +177,26 @@ "node": ">=4" } }, + "node_modules/@datadog/datadog-api-client": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@datadog/datadog-api-client/-/datadog-api-client-1.30.0.tgz", + "integrity": "sha512-Xa/7rpnM+QwL38+BiA7J9GUOA/ikQK4AhNMkqAGj0mLuyrZQv9oAlqxXuP7ZbdrdhB76h8ZX0Js13xEyvj/NaA==", + "license": "Apache-2.0", + "dependencies": { + "@types/buffer-from": "^1.1.0", + "@types/node": "*", + "@types/pako": "^1.0.3", + "buffer-from": "^1.1.2", + "cross-fetch": "^3.1.5", + "es6-promise": "^4.2.8", + "form-data": "^4.0.0", + "loglevel": "^1.8.1", + "pako": "^2.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@eslint-community/eslint-plugin-eslint-comments": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.3.0.tgz", @@ -694,10 +715,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -748,6 +778,19 @@ "node": ">= 14" } }, + "node_modules/datadog-metrics": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/datadog-metrics/-/datadog-metrics-0.11.4.tgz", + "integrity": "sha512-n84qioHgranFc2KSkEvxWq3yNX43iFp9h+/7WaISMpu9GPL9KrmU9FgX4OSMc9I5tU52J2JU7yCfbpjDFT3/KQ==", + "license": "MIT", + "dependencies": { + "@datadog/datadog-api-client": "^1.16.0", + "debug": "^4.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -2498,9 +2541,9 @@ "license": "MIT" }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9e1997aca94..6472ab5039b 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "domains:trust-self-signed-certificate:linux": "cd ./scripts/local-domains/ && sudo docker cp caddy:/data/caddy/pki/authorities/local/root.crt /usr/local/share/ca-certificates/root.crt && sudo update-ca-certificates --fresh" }, "dependencies": { + "datadog-metrics": "^0.11.4", "npm-run-all2": "^6.0.0" }, "devDependencies": {