diff --git a/eslint.config.mjs b/eslint.config.mjs index 9d79a8d1f1..458e492fcd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -690,7 +690,9 @@ export default [ // The following are false positives that are supported in Node.js 0.8.0 ignores: [ 'JSON', + 'JSON.parse', 'JSON.stringify', + 'Object.keys', 'parseInt', 'String', ], @@ -700,6 +702,7 @@ export default [ ignores: [ 'array-prototype-indexof', 'json', + 'object-keys', ], }], 'no-var': 'off', // Only supported in Node.js 6+ diff --git a/init.js b/init.js index 0ab55eb114..701996d43a 100644 --- a/init.js +++ b/init.js @@ -1,5 +1,23 @@ 'use strict' +// In PM2 cluster mode, per-app env vars arrive as a `pm2_env` +// JSON string after --require has already run so we extract them +// manually if present. +var pm2EnvStr = process.env.pm2_env +if (typeof pm2EnvStr === 'string') { + try { + var pm2Config = JSON.parse(pm2EnvStr) + var pm2Keys = Object.keys(pm2Config) + for (var i = 0; i < pm2Keys.length; i++) { + var k = pm2Keys[i] + var v = pm2Config[k] + if (v != null) { + process.env[k] = String(v) + } + } + } catch (e) {} +} + var guard = require('./packages/dd-trace/src/guardrails') module.exports = guard(function () { diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index cc2bc9f743..60d4666628 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -272,6 +272,56 @@ describe('init.js', () => { testInjectionScenarios('require', 'init.js', false) testRuntimeVersionChecks('require', 'init.js') + + describe('PM2 cluster mode', () => { + useEnv({ NODE_OPTIONS: '--require dd-trace/init' }) + + afterEach(() => { + delete process.env.pm2_env + }) + + function checkEnv (expectedValues) { + return testFile('init/pm2-env.js', out => { + const env = JSON.parse(out.trim()) + for (const [key, value] of Object.entries(expectedValues)) { + assert.strictEqual(env[key], value, `expected env.${key} to equal ${value}`) + } + }, [], '') + } + + it('applies all env vars from pm2_env blob to process.env', () => { + process.env.pm2_env = JSON.stringify({ DD_SERVICE: 'pm2-svc', DD_ENV: 'pm2-env', MY_APP_VAR: 'hello' }) + return checkEnv({ DD_SERVICE: 'pm2-svc', DD_ENV: 'pm2-env', MY_APP_VAR: 'hello' }) + }) + + it('coerces non-string values to strings', () => { + process.env.pm2_env = JSON.stringify({ DD_TRACE_SAMPLE_RATE: 0.5 }) + return checkEnv({ DD_TRACE_SAMPLE_RATE: '0.5' }) + }) + + it('skips keys with null values', () => { + process.env.pm2_env = JSON.stringify({ DD_SERVICE: null }) + return checkEnv({ DD_SERVICE: undefined }) + }) + + it('does not crash on malformed pm2_env JSON', () => { + process.env.pm2_env = 'not-valid-json' + return checkEnv({ DD_SERVICE: undefined }) + }) + + it('does nothing when pm2_env is absent', () => { + return checkEnv({ DD_SERVICE: undefined }) + }) + + describe('when env vars are already set', () => { + useEnv({ DD_SERVICE: 'original-service', MY_APP_VAR: 'original' }) + + it('overwrites existing env vars with pm2_env values', () => { + process.env.pm2_env = JSON.stringify({ DD_SERVICE: 'pm2-service', MY_APP_VAR: 'pm2-value' }) + return checkEnv({ DD_SERVICE: 'pm2-service', MY_APP_VAR: 'pm2-value' }) + }) + }) + }) }) // ESM is not supportable prior to Node.js 14.13.1 on the 14.x line, diff --git a/integration-tests/init/pm2-env.js b/integration-tests/init/pm2-env.js new file mode 100644 index 0000000000..222085ca01 --- /dev/null +++ b/integration-tests/init/pm2-env.js @@ -0,0 +1,10 @@ +'use strict' + +// eslint-disable-next-line no-console +console.log(JSON.stringify({ + DD_SERVICE: process.env.DD_SERVICE, + DD_ENV: process.env.DD_ENV, + DD_TRACE_SAMPLE_RATE: process.env.DD_TRACE_SAMPLE_RATE, + MY_APP_VAR: process.env.MY_APP_VAR, +})) +process.exit()