From dfe847e89762413d0e0546de4349761ac15ff72d Mon Sep 17 00:00:00 2001 From: Munir Abdinur Date: Thu, 12 Jun 2025 10:32:41 -0400 Subject: [PATCH] chore(ssi): track the source of installation --- init.js | 5 +- integration-tests/helpers/index.js | 13 ++++-- integration-tests/init.spec.js | 46 +++++++++---------- integration-tests/init/instrument.js | 2 + integration-tests/init/instrument.mjs | 2 + integration-tests/init/trace.js | 2 + integration-tests/package-guardrails.spec.js | 25 +++++----- integration-tests/package-guardrails/index.js | 8 +++- packages/dd-trace/src/config.js | 11 ++++- packages/dd-trace/src/constants.js | 3 +- packages/dd-trace/src/guardrails/index.js | 2 + packages/dd-trace/src/telemetry/telemetry.js | 5 +- packages/dd-trace/test/config.spec.js | 7 +++ 13 files changed, 86 insertions(+), 45 deletions(-) diff --git a/init.js b/init.js index 625d493b3b1..6f1f664df93 100644 --- a/init.js +++ b/init.js @@ -5,5 +5,8 @@ var guard = require('./packages/dd-trace/src/guardrails') module.exports = guard(function () { - return require('.').init() + var INSTRUMENTED_BY_SSI = require('./packages/dd-trace/src/constants').INSTRUMENTED_BY_SSI + var obj = {} + obj[INSTRUMENTED_BY_SSI] = 'ssi' + return require('.').init(obj) }) diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index 1c3a2b454a4..e37a0547c46 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -18,7 +18,7 @@ const hookFile = 'dd-trace/loader-hook.mjs' // This is set by the setShouldKill function let shouldKill -async function runAndCheckOutput (filename, cwd, expectedOut) { +async function runAndCheckOutput (filename, cwd, expectedOut, expectedSource) { const proc = spawn(process.execPath, [filename], { cwd, stdio: 'pipe' }) const pid = proc.pid let out = await new Promise((resolve, reject) => { @@ -42,7 +42,12 @@ async function runAndCheckOutput (filename, cwd, expectedOut) { // Debug adds this, which we don't care about in these tests out = out.replace('Flushing 0 metrics via HTTP\n', '') } - assert.strictEqual(out, expectedOut) + assert.match(out, new RegExp(expectedOut), `output "${out} does not contain expected output "${expectedOut}"`) + } + + if (expectedSource) { + assert.match(out, new RegExp(`instrumentation source: ${expectedSource}`), + `Expected the process to output "${expectedSource}", but logs only contain: "${out}"`) } return pid } @@ -51,10 +56,10 @@ async function runAndCheckOutput (filename, cwd, expectedOut) { let sandbox // This _must_ be used with the useSandbox function -async function runAndCheckWithTelemetry (filename, expectedOut, ...expectedTelemetryPoints) { +async function runAndCheckWithTelemetry (filename, expectedOut, expectedTelemetryPoints, expectedSource) { const cwd = sandbox.folder const cleanup = telemetryForwarder(expectedTelemetryPoints) - const pid = await runAndCheckOutput(filename, cwd, expectedOut) + const pid = await runAndCheckOutput(filename, cwd, expectedOut, expectedSource) const msgs = await cleanup() if (expectedTelemetryPoints.length === 0) { // assert no telemetry sent diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index b04a64dd0e9..51a4a0b5432 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -36,18 +36,18 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { if (currentVersionIsSupported) { context('without DD_INJECTION_ENABLED', () => { - it('should initialize the tracer', () => doTest('init/trace.js', 'true\n')) - it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n')) + it('should initialize the tracer', () => doTest('init/trace.js', 'true\n', [], 'ssi')) + it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n', [], 'ssi')) it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - doTest('init/instrument.mjs', `${esmWorks}\n`)) + doTest('init/instrument.mjs', `${esmWorks}\n`, [])) }) } context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) - it('should not initialize the tracer', () => doTest('init/trace.js', 'false\n')) - it('should not initialize instrumentation', () => doTest('init/instrument.js', 'false\n')) - it('should not initialize ESM instrumentation', () => doTest('init/instrument.mjs', 'false\n')) + it('should not initialize the tracer', () => doTest('init/trace.js', 'false\n', [])) + it('should not initialize instrumentation', () => doTest('init/instrument.js', 'false\n', [])) + it('should not initialize ESM instrumentation', () => doTest('init/instrument.mjs', 'false\n', [])) }) }) context('when dd-trace in the app dir', () => { @@ -55,18 +55,18 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { useEnv({ NODE_OPTIONS }) context('without DD_INJECTION_ENABLED', () => { - it('should initialize the tracer', () => doTest('init/trace.js', 'true\n')) - it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n')) + it('should initialize the tracer', () => doTest('init/trace.js', 'true\n', [], 'ssi')) + it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n', [], 'ssi')) it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - doTest('init/instrument.mjs', `${esmWorks}\n`)) + doTest('init/instrument.mjs', `${esmWorks}\n`, [])) }) context('with DD_INJECTION_ENABLED', () => { - useEnv({ DD_INJECTION_ENABLED }) + useEnv({ DD_INJECTION_ENABLED, DD_TRACE_DEBUG }) - it('should initialize the tracer', () => doTest('init/trace.js', 'true\n', ...telemetryGood)) - it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n', ...telemetryGood)) + it('should initialize the tracer', () => doTest('init/trace.js', 'true\n', telemetryGood, 'ssi')) + it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n', telemetryGood, 'ssi')) it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - doTest('init/instrument.mjs', `${esmWorks}\n`, ...telemetryGood)) + doTest('init/instrument.mjs', `${esmWorks}\n`, telemetryGood, 'ssi')) }) }) }) @@ -90,13 +90,13 @@ function testRuntimeVersionChecks (arg, filename) { useEnv({ NODE_OPTIONS }) it('should not initialize the tracer', () => - doTest('false\n')) + doTest('false\n', [])) context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) context('without debug', () => { - it('should not initialize the tracer', () => doTest('false\n', ...telemetryAbort)) - it('should initialize the tracer, if DD_INJECT_FORCE', () => doTestForced('true\n', ...telemetryForced)) + it('should not initialize the tracer', () => doTest('false\n', telemetryAbort)) + it('should initialize the tracer, if DD_INJECT_FORCE', () => doTestForced('true\n', telemetryForced)) }) context('with debug', () => { useEnv({ DD_TRACE_DEBUG }) @@ -106,7 +106,7 @@ function testRuntimeVersionChecks (arg, filename) { Found incompatible runtime nodejs ${process.versions.node}, Supported runtimes: nodejs \ >=18. false -`, ...telemetryAbort)) +`, telemetryAbort)) it('should initialize the tracer, if DD_INJECT_FORCE', () => doTestForced(`Aborting application instrumentation due to incompatible_runtime. Found incompatible runtime nodejs ${process.versions.node}, Supported runtimes: nodejs \ @@ -114,7 +114,7 @@ Found incompatible runtime nodejs ${process.versions.node}, Supported runtimes: DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing. Application instrumentation bootstrapping complete true -`, ...telemetryForced)) +`, telemetryForced)) }) }) }) @@ -122,22 +122,22 @@ true context('when node version is more than engines field', () => { useEnv({ NODE_OPTIONS }) - it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => doTest('true\n')) + it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => doTest('true\n', [], 'ssi')) context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) context('without debug', () => { - it('should initialize the tracer', () => doTest('true\n', ...telemetryGood)) + it('should initialize the tracer', () => doTest('true\n', telemetryGood, 'ssi')) it('should initialize the tracer, if DD_INJECT_FORCE', () => - doTestForced('true\n', ...telemetryGood)) + doTestForced('true\n', telemetryGood, 'ssi')) }) context('with debug', () => { useEnv({ DD_TRACE_DEBUG }) it('should initialize the tracer', () => - doTest('Application instrumentation bootstrapping complete\ntrue\n', ...telemetryGood)) + doTest('Application instrumentation bootstrapping complete\ntrue\n', telemetryGood, 'ssi')) it('should initialize the tracer, if DD_INJECT_FORCE', () => - doTestForced('Application instrumentation bootstrapping complete\ntrue\n', ...telemetryGood)) + doTestForced('Application instrumentation bootstrapping complete\ntrue\n', telemetryGood, 'ssi')) }) }) }) diff --git a/integration-tests/init/instrument.js b/integration-tests/init/instrument.js index 55e5d28f450..b1114e6237b 100644 --- a/integration-tests/init/instrument.js +++ b/integration-tests/init/instrument.js @@ -15,6 +15,8 @@ const server = http.createServer((req, res) => { server.close() // eslint-disable-next-line no-console console.log(gotEvent) + // eslint-disable-next-line no-console + console.log('instrumentation source:', global._ddtrace._tracer._config.instrumentationSource) process.exit() }) }) diff --git a/integration-tests/init/instrument.mjs b/integration-tests/init/instrument.mjs index bddaf6ef13a..8edfa4e0bc2 100644 --- a/integration-tests/init/instrument.mjs +++ b/integration-tests/init/instrument.mjs @@ -15,6 +15,8 @@ const server = http.createServer((req, res) => { server.close() // eslint-disable-next-line no-console console.log(gotEvent) + // eslint-disable-next-line no-console + console.log('instrumentation source:', global._ddtrace._tracer._config.instrumentationSource) process.exit() }) }) diff --git a/integration-tests/init/trace.js b/integration-tests/init/trace.js index 9c9c5d731d9..a05665b5ea9 100644 --- a/integration-tests/init/trace.js +++ b/integration-tests/init/trace.js @@ -1,3 +1,5 @@ // eslint-disable-next-line no-console console.log(!!global._ddtrace) +// eslint-disable-next-line no-console +console.log('instrumentation source:', global._ddtrace._tracer._config.instrumentationSource) process.exit() diff --git a/integration-tests/package-guardrails.spec.js b/integration-tests/package-guardrails.spec.js index 4ee05c033cb..a2c366e4104 100644 --- a/integration-tests/package-guardrails.spec.js +++ b/integration-tests/package-guardrails.spec.js @@ -29,12 +29,13 @@ describe('package guardrails', () => { useEnv({ DD_INJECTION_ENABLED }) it('should not instrument the package, and send telemetry', () => runTest('false\n', - 'complete', 'injection_forced:false', - 'abort.integration', 'integration:bluebird,integration_version:1.0.0' + ['complete', 'injection_forced:false', + 'abort.integration', 'integration:bluebird,integration_version:1.0.0' + ] )) }) context('with logging disabled', () => { - it('should not instrument the package', () => runTest('false\n')) + it('should not instrument the package', () => runTest('false\n', [])) }) context('with logging enabled', () => { useEnv({ DD_TRACE_DEBUG }) @@ -42,31 +43,31 @@ describe('package guardrails', () => { runTest(`Application instrumentation bootstrapping complete Found incompatible integration version: bluebird@1.0.0 false -`)) +`, [])) }) }) context('when package is in range', () => { context('when bluebird is 2.9.0', () => { useSandbox(['bluebird@2.9.0']) - it('should instrument the package', () => runTest('true\n')) + it('should instrument the package', () => runTest('true\n', [], 'ssi')) }) context('when bluebird is 3.7.2', () => { useSandbox(['bluebird@3.7.2']) - it('should instrument the package', () => runTest('true\n')) + it('should instrument the package', () => runTest('true\n', [], 'ssi')) }) }) context('when package is in range (fastify)', () => { context('when fastify is latest', () => { useSandbox(['fastify']) - it('should instrument the package', () => runTest('true\n')) + it('should instrument the package', () => runTest('true\n', [], 'ssi')) }) context('when fastify is latest and logging enabled', () => { useSandbox(['fastify']) useEnv({ DD_TRACE_DEBUG }) it('should instrument the package', () => - runTest('Application instrumentation bootstrapping complete\ntrue\n')) + runTest('Application instrumentation bootstrapping complete\ntrue\n', [], 'ssi')) }) }) @@ -88,13 +89,13 @@ addHook({ name: 'bluebird', versions: ['*'] }, Promise => { useEnv({ DD_INJECTION_ENABLED }) it('should not instrument the package, and send telemetry', () => runTest('false\n', - 'complete', 'injection_forced:false', - 'error', 'error_type:ReferenceError,integration:bluebird,integration_version:3.7.2' + ['complete', 'injection_forced:false', + 'error', 'error_type:ReferenceError,integration:bluebird,integration_version:3.7.2'] )) }) context('with logging disabled', () => { - it('should not instrument the package', () => runTest('false\n')) + it('should not instrument the package', () => runTest('false\n', [])) }) context('with logging enabled', () => { @@ -107,7 +108,7 @@ Error during ddtrace instrumentation of application, aborting. ReferenceError: this is a test error at `)) assert.ok(log.includes('\nfalse\n')) - })) + }, [])) }) }) }) diff --git a/integration-tests/package-guardrails/index.js b/integration-tests/package-guardrails/index.js index 0742ebc87e0..a38a8d2f2d5 100644 --- a/integration-tests/package-guardrails/index.js +++ b/integration-tests/package-guardrails/index.js @@ -1,15 +1,19 @@ 'use strict' -/* eslint-disable no-console */ - try { const P = require('bluebird') const isWrapped = P.prototype._then.toString().includes('AsyncResource') + // eslint-disable-next-line no-console console.log(isWrapped) } catch (e) { const fastify = require('fastify') + // eslint-disable-next-line no-console console.log(fastify.toString().startsWith('function fastifyWithTrace')) } +if (global._ddtrace) { + // eslint-disable-next-line no-console + console.log('instrumentation source:', global._ddtrace._tracer._config.instrumentationSource) +} diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 4f97b6038b6..154008e0956 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -15,7 +15,9 @@ const { getGitMetadataFromGitProperties, removeUserSensitiveInfo } = require('./ const { updateConfig } = require('./telemetry') const telemetryMetrics = require('./telemetry/metrics') const { isInServerlessEnvironment, getIsGCPFunction, getIsAzureFunction } = require('./serverless') -const { ORIGIN_KEY, GRPC_CLIENT_ERROR_STATUSES, GRPC_SERVER_ERROR_STATUSES } = require('./constants') +const { + ORIGIN_KEY, GRPC_CLIENT_ERROR_STATUSES, GRPC_SERVER_ERROR_STATUSES, INSTRUMENTED_BY_SSI +} = require('./constants') const { appendRules } = require('./payload-tagging/config') const { getEnvironmentVariable, getEnvironmentVariables } = require('./config-helper') @@ -522,6 +524,8 @@ class Config { this._setValue(defaults, 'iast.telemetryVerbosity', 'INFORMATION') this._setValue(defaults, 'iast.stackTrace.enabled', true) this._setValue(defaults, 'injectionEnabled', []) + this._setValue(defaults, 'instrumentationSource', 'manual') + this._setValue(defaults, 'injectForce', null) this._setValue(defaults, 'isAzureFunction', false) this._setValue(defaults, 'isCiVisibility', false) this._setValue(defaults, 'isEarlyFlakeDetectionEnabled', false) @@ -701,6 +705,7 @@ class Config { DD_IAST_TELEMETRY_VERBOSITY, DD_IAST_STACK_TRACE_ENABLED, DD_INJECTION_ENABLED, + DD_INJECT_FORCE, DD_INSTRUMENTATION_TELEMETRY_ENABLED, DD_INSTRUMENTATION_CONFIG_ID, DD_LOGS_INJECTION, @@ -892,6 +897,7 @@ class Config { this._setString(env, 'iast.telemetryVerbosity', DD_IAST_TELEMETRY_VERBOSITY) this._setBoolean(env, 'iast.stackTrace.enabled', DD_IAST_STACK_TRACE_ENABLED) this._setArray(env, 'injectionEnabled', DD_INJECTION_ENABLED) + this._setBoolean(env, 'injectForce', DD_INJECT_FORCE) this._setBoolean(env, 'isAzureFunction', getIsAzureFunction()) this._setBoolean(env, 'isGCPFunction', getIsGCPFunction()) this._setValue(env, 'langchain.spanCharLimit', maybeInt(DD_LANGCHAIN_SPAN_CHAR_LIMIT)) @@ -1127,6 +1133,9 @@ class Config { this._setValue(opts, 'iast.securityControlsConfiguration', options.iast?.securityControlsConfiguration) this._setBoolean(opts, 'iast.stackTrace.enabled', options.iast?.stackTrace?.enabled) this._setString(opts, 'iast.telemetryVerbosity', options.iast && options.iast.telemetryVerbosity) + if (options[INSTRUMENTED_BY_SSI]) { + this._setString(opts, 'instrumentationSource', options[INSTRUMENTED_BY_SSI]) + } this._setBoolean(opts, 'isCiVisibility', options.isCiVisibility) this._setBoolean(opts, 'legacyBaggageEnabled', options.legacyBaggageEnabled) this._setBoolean(opts, 'llmobs.agentlessEnabled', options.llmobs?.agentlessEnabled) diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index c184e595237..dbbdede3cb5 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -53,5 +53,6 @@ module.exports = { SPAN_POINTER_DIRECTION: Object.freeze({ UPSTREAM: 'u', DOWNSTREAM: 'd' - }) + }), + INSTRUMENTED_BY_SSI: Symbol('_dd.instrumented.by.ssi') } diff --git a/packages/dd-trace/src/guardrails/index.js b/packages/dd-trace/src/guardrails/index.js index 179262f154e..c941cc244d9 100644 --- a/packages/dd-trace/src/guardrails/index.js +++ b/packages/dd-trace/src/guardrails/index.js @@ -54,6 +54,8 @@ function guard (fn) { } if (!clobberBailout && (!initBailout || forced)) { + // Ensure the instrumentation source is set for the current process and potential + // child processes. var result = fn() telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) log.info('Application instrumentation bootstrapping complete') diff --git a/packages/dd-trace/src/telemetry/telemetry.js b/packages/dd-trace/src/telemetry/telemetry.js index 8959434161c..b1273c4fea9 100644 --- a/packages/dd-trace/src/telemetry/telemetry.js +++ b/packages/dd-trace/src/telemetry/telemetry.js @@ -321,7 +321,10 @@ const nameMapping = { clientIpHeader: 'DD_TRACE_CLIENT_IP_HEADER', 'grpc.client.error.statuses': 'DD_GRPC_CLIENT_ERROR_STATUSES', 'grpc.server.error.statuses': 'DD_GRPC_SERVER_ERROR_STATUSES', - traceId128BitLoggingEnabled: 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED' + traceId128BitLoggingEnabled: 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED', + instrumentationSource: 'instrumentation_source', + injectionEnabled: 'ssi_injection_enabled', + injectForce: 'ssi_forced_injection_enabled' } const namesNeedFormatting = new Set(['DD_TAGS', 'peerServiceMapping', 'serviceMapping']) diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 363fb90d746..edd09568d33 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -341,6 +341,9 @@ describe('Config', () => { expect(config).to.have.nested.property('llmobs.mlApp', undefined) expect(config).to.have.nested.property('llmobs.agentlessEnabled', undefined) expect(config).to.have.nested.property('llmobs.enabled', false) + expect(config).to.have.nested.deep.property('injectionEnabled', []) + expect(config).to.have.nested.property('instrumentationSource', 'manual') + expect(config).to.have.nested.property('injectForce', null) expect(updateConfig).to.be.calledOnce @@ -406,7 +409,9 @@ describe('Config', () => { { name: 'iast.securityControlsConfiguration', value: null, origin: 'default' }, { name: 'iast.telemetryVerbosity', value: 'INFORMATION', origin: 'default' }, { name: 'iast.stackTrace.enabled', value: true, origin: 'default' }, + { name: 'instrumentationSource', value: 'manual', origin: 'default' }, { name: 'injectionEnabled', value: [], origin: 'default' }, + { name: 'injectForce', value: null, origin: 'default' }, { name: 'isCiVisibility', value: false, origin: 'default' }, { name: 'isEarlyFlakeDetectionEnabled', value: false, origin: 'default' }, { name: 'isFlakyTestRetriesEnabled', value: false, origin: 'default' }, @@ -601,6 +606,7 @@ describe('Config', () => { process.env.DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED = 'true' process.env.DD_PROFILING_ENABLED = 'true' process.env.DD_INJECTION_ENABLED = 'profiler' + process.env.DD_INJECT_FORCE = 'false' process.env.DD_API_SECURITY_ENABLED = 'true' process.env.DD_API_SECURITY_SAMPLE_DELAY = '25' process.env.DD_INSTRUMENTATION_INSTALL_ID = '68e75c48-57ca-4a12-adfc-575c4b05fcbe' @@ -787,6 +793,7 @@ describe('Config', () => { { name: 'iast.stackTrace.enabled', value: false, origin: 'env_var' }, { name: 'instrumentation_config_id', value: 'abcdef123', origin: 'env_var' }, { name: 'injectionEnabled', value: ['profiler'], origin: 'env_var' }, + { name: 'injectForce', value: false, origin: 'env_var' }, { name: 'isGCPFunction', value: false, origin: 'env_var' }, { name: 'middlewareTracingEnabled', value: false, origin: 'env_var' }, { name: 'peerServiceMapping', value: process.env.DD_TRACE_PEER_SERVICE_MAPPING, origin: 'env_var' },