UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

481 lines (417 loc) 16.2 kB
'use strict' const NoopProxy = require('./noop/proxy') const { features } = require('./feature-registry') const DatadogTracer = require('./tracer') const getConfig = require('./config') const { getEnvironmentVariable } = require('./config/helper') const runtimeMetrics = require('./runtime_metrics') const log = require('./log') const { setStartupLogPluginManager, startupLog } = require('./startup-log') const DynamicInstrumentation = require('./debugger') const telemetry = require('./telemetry') const nomenclature = require('./service-naming') const PluginManager = require('./plugin_manager') const NoopDogStatsDClient = require('./noop/dogstatsd') const { IS_SERVERLESS } = require('./serverless') const processTags = require('./process-tags') const { isTrue } = require('./util') const { setBaggageItem, getBaggageItem, getAllBaggageItems, removeBaggageItem, removeAllBaggageItems, } = require('./baggage') const VALIDATION_MANIFEST_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_MANIFEST_FILE' const VALIDATION_MODE_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_MODE' const VALIDATION_OUTPUT_ENV = '_DD_TEST_OPTIMIZATION_VALIDATION_OUTPUT_DIR' const ORDINARY_TRACING_DISABLED_MESSAGE = 'Ordinary dd-trace application tracing initialization is disabled during offline Test Optimization validation. ' + 'The CI test process must initialize through dd-trace/ci/init.' const OFFLINE_VALIDATION_EXPORTERS = new Set([ 'ci_validation', 'cucumber_worker', 'jest_worker', 'mocha_worker', 'playwright_worker', 'vitest_worker', ]) const FEATURE_STATE_NOOP = 0 const FEATURE_STATE_LAZY = 1 const FEATURE_STATE_ACTIVE = 2 class LazyModule { constructor (provider) { this.provider = provider } /** * @param {import('./config/config-base')} config - Tracer configuration */ enable (config, ...args) { this.module = this.provider() this.module.enable(config, ...args) } disable () { this.module?.disable() } } function lazyProxy (...args) { if (IS_SERVERLESS === false) { defineEagerly(...args) } else { defineLazily(...args) } } function defineEagerly (obj, property, getClass, ...args) { const RealClass = getClass() obj[property] = new RealClass(...args) } function defineLazily (obj, property, getClass, ...args) { Reflect.defineProperty(obj, property, { get () { const RealClass = getClass() const value = new RealClass(...args) Reflect.defineProperty(obj, property, { value, configurable: true, enumerable: true }) return value }, configurable: true, enumerable: true, }) } class Tracer extends NoopProxy { /** @type {Record<string, number> | undefined} */ #featureStates constructor () { super() this._initialized = false this._nomenclature = nomenclature this._pluginManager = new PluginManager(this) this.dogstatsd = new NoopDogStatsDClient() this._tracingInitialized = false this._flare = new LazyModule(() => require('./flare')) this.setBaggageItem = setBaggageItem this.getBaggageItem = getBaggageItem this.getAllBaggageItems = getAllBaggageItems this.removeBaggageItem = removeBaggageItem this.removeAllBaggageItems = removeAllBaggageItems // these requires must work with esm bundler this._modules = { appsec: new LazyModule(() => require('./appsec')), aiguard: new LazyModule(() => require('./aiguard')), iast: new LazyModule(() => require('./appsec/iast')), llmobs: new LazyModule(() => require('./llmobs')), rewriter: new LazyModule(() => require('./appsec/iast/taint-tracking/rewriter')), } for (const feature of Object.values(features)) { this._modules[feature.name] = new LazyModule(feature.factory) } } /** * @override */ init (options) { if (this._initialized) return this this._initialized = true if (isOfflineTestOptimizationValidation() && !isOfflineValidationExporter(options)) { process.stderr.write(`${ORDINARY_TRACING_DISABLED_MESSAGE}\n`) return this } try { const config = getConfig(options) // TODO: support dynamic code config // Add config dependent process tags processTags.initialize(config) // Configure propagation hash manager for process tags + container tags const propagationHash = require('./propagation-hash') propagationHash.configure(config) if (config.DD_CRASHTRACKING_ENABLED) { try { require('./crashtracking').start(config) } catch (e) { log.warn('Crashtracking is not available in this environment', e) } } if (config.DD_HEAP_SNAPSHOT_COUNT > 0) { require('./heap_snapshots').start(config) } telemetry.start(config, this._pluginManager) if (config.dogstatsd && !isOfflineTestOptimizationValidation()) { // Custom Metrics lazyProxy(this, 'dogstatsd', () => require('./dogstatsd').CustomMetrics, config) } if (config.DD_TRACE_SPAN_LEAK_DEBUG > 0) { const spanleak = require('./spanleak') if (config.DD_TRACE_SPAN_LEAK_DEBUG === spanleak.MODES.LOG) { spanleak.enableLogging() } else if (config.DD_TRACE_SPAN_LEAK_DEBUG === spanleak.MODES.GC_AND_LOG) { spanleak.enableGarbageCollection() } spanleak.startScrubber() } if (config.remoteConfig.DD_REMOTE_CONFIGURATION_ENABLED && !config.isCiVisibility) { const RemoteConfig = require('./remote_config') const rc = new RemoteConfig(config) const tracingRemoteConfig = require('./config/remote_config') tracingRemoteConfig.enable(rc, config, () => { this.#updateTracing(config) this.#updateDebugger(config, rc) }) rc.setProductHandler('AGENT_CONFIG', (action, conf) => { if (!conf?.name?.startsWith('flare-log-level.')) return if (action === 'unapply') { this._flare.disable() } else if (conf.config?.log_level) { this._flare.enable(config) this._flare.module.prepare(conf.config.log_level) } }) rc.setProductHandler('AGENT_TASK', (action, conf) => { if (action === 'unapply' || !conf) return if (conf.task_type !== 'tracer_flare' || !conf.args) return this._flare.enable(config) this._flare.module.send(conf.args) }) if (this._modules.appsec) { const appsecRemoteConfig = require('./appsec/remote_config') appsecRemoteConfig.enable(rc, config, this._modules.appsec) } if (config.dynamicInstrumentation.enabled) { DynamicInstrumentation.start(config, rc) } for (const feature of Object.values(features)) { feature.remoteConfig?.(rc, config, this) } } if (config.profiling.DD_PROFILING_ENABLED === 'true') { this._profilerStarted = this._startProfiler(config) } else { this._profilerStarted = false if (config.profiling.DD_PROFILING_ENABLED === 'auto') { const { SSIHeuristics } = require('./profiling/ssi-heuristics') const ssiHeuristics = new SSIHeuristics(config) ssiHeuristics.start() ssiHeuristics.onTriggered(() => { this._startProfiler(config) ssiHeuristics.onTriggered() // deregister this callback }) } } // OTel logs/metrics pipelines must be initialized BEFORE runtimeMetrics.start so that // when the OTLP runtime metrics module calls metrics.getMeterProvider(), it gets the // real provider, otherwise instruments register on the noop provider and never export. if (config.DD_LOGS_OTEL_ENABLED) { const { initializeOpenTelemetryLogs } = require('./opentelemetry/logs') initializeOpenTelemetryLogs(config) } if (config.DD_METRICS_OTEL_ENABLED) { const { initializeOpenTelemetryMetrics } = require('./opentelemetry/metrics') initializeOpenTelemetryMetrics(config) } if (config.runtimeMetrics.enabled) { runtimeMetrics.start(config) } this.#updateTracing(config) this._modules.rewriter.enable(config) if (config.DD_TRACE_ENABLED && config.testOptimization.DD_CIVISIBILITY_MANUAL_API_ENABLED) { const TestApiManualPlugin = require('./ci-visibility/test-api-manual/test-api-manual-plugin') this._testApiManualPlugin = new TestApiManualPlugin(this) // `shouldGetEnvironmentData` is passed as false so that we only lazily calculate it // This is the only place where we need to do this because the rest of the plugins // are lazily configured when the library is imported. this._testApiManualPlugin.configure({ ...config, enabled: true }, false) } if (config.DD_AGENTLESS_LOG_SUBMISSION_ENABLED) { if (config.DD_API_KEY) { const LogSubmissionPlugin = require('./ci-visibility/log-submission/log-submission-plugin') const automaticLogPlugin = new LogSubmissionPlugin(this) automaticLogPlugin.configure({ ...config, enabled: true }) } else { log.warn( // eslint-disable-next-line @stylistic/max-len 'DD_AGENTLESS_LOG_SUBMISSION_ENABLED is set, but DD_API_KEY is undefined, so no automatic log submission will be performed.' ) } } if (config.testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) { const getDynamicInstrumentationClient = require('./ci-visibility/dynamic-instrumentation') // We instantiate the client but do not start the Worker here. The worker is started lazily getDynamicInstrumentationClient(config) } } catch (e) { log.error('Error initializing tracer', e) // TODO: Should we stop everything started so far? } return this } /** * @param {import('./config/config-base')} config - Tracer configuration */ _startProfiler (config) { // do not stop tracer initialization if the profiler fails to be imported try { return require('./profiler').start(config) } catch (error) { log.error( 'Error starting profiler. For troubleshooting tips, see <https://dtdg.co/nodejs-profiler-troubleshooting>', error ) return false } } /** * @param {(typeof features)[string]} feature * @param {import('./config/config-base')} config */ #enableFeature (feature, config) { const states = this.#featureStates ??= {} const state = states[feature.name] ?? FEATURE_STATE_NOOP if (state === FEATURE_STATE_ACTIVE || state === FEATURE_STATE_LAZY) return states[feature.name] = FEATURE_STATE_LAZY Reflect.defineProperty(this, feature.name, { get: () => { const Provider = feature.provider() const provider = new Provider(this._tracer, config) this._modules[feature.name].enable(config) Reflect.defineProperty(this, feature.name, { value: provider, configurable: true, enumerable: true, }) states[feature.name] = FEATURE_STATE_ACTIVE return provider }, configurable: true, enumerable: true, }) } /** * @param {import('./config/config-base')} config - Tracer configuration */ #updateTracing (config) { if (config.DD_TRACE_ENABLED !== false) { if (config.appsec.enabled) { this._modules.appsec.enable(config) } if (config.llmobs.DD_LLMOBS_ENABLED) { this._modules.llmobs.enable(config) } if (!this._tracingInitialized) { const prioritySampler = config.apmTracingEnabled === false ? require('./standalone').configure(config) : undefined this._tracer = new DatadogTracer(config, prioritySampler) this.dataStreamsCheckpointer = this._tracer.dataStreamsCheckpointer lazyProxy(this, 'appsec', () => require('./appsec/sdk'), this._tracer, config) lazyProxy(this, 'llmobs', () => require('./llmobs/sdk'), this._tracer, this._modules.llmobs, config) if (config.experimental?.aiguard?.enabled) { lazyProxy(this, 'aiguard', () => require('./aiguard/sdk'), this._tracer, config) } this._tracingInitialized = true } if (config.experimental?.aiguard?.enabled) { this._modules.aiguard.enable(this._tracer, config) } if (config.iast.enabled) { this._modules.iast.enable(config, this._tracer) } // This needs to be after the IAST module is enabled } else if (this._tracingInitialized) { this._modules.appsec.disable() this._modules.aiguard.disable() this._modules.iast.disable() this._modules.llmobs.disable() } for (const feature of Object.values(features)) { if (feature.isEnabled(config)) this.#enableFeature(feature, config) } if (this._tracingInitialized) { this._tracer.configure(config) this._pluginManager.configure(config) DynamicInstrumentation.configure(config) setStartupLogPluginManager(this._pluginManager) startupLog() } } /** * Updates the debugger (Dynamic Instrumentation) state based on remote config changes. * Handles starting, stopping, and reconfiguring the debugger dynamically. * * @param {object} config - The tracer configuration object * @param {object} rc - The RemoteConfig instance */ #updateDebugger (config, rc) { const shouldBeEnabled = config.dynamicInstrumentation.enabled const isCurrentlyStarted = DynamicInstrumentation.isStarted() if (shouldBeEnabled) { if (isCurrentlyStarted) { log.debug('[proxy] Reconfiguring Dynamic Instrumentation via remote config') DynamicInstrumentation.configure(config) } else { log.debug('[proxy] Starting Dynamic Instrumentation via remote config') DynamicInstrumentation.start(config, rc) } } else if (isCurrentlyStarted) { log.debug('[proxy] Stopping Dynamic Instrumentation via remote config') DynamicInstrumentation.stop() } } /** * @override */ get profiling () { // Lazily require the profiler module and cache the result. If profiling // is not enabled, runWithLabels still works as a passthrough (just calls fn()). const profilerModule = require('./profiler') const profiling = { setCustomLabelKeys (keys) { profilerModule.setCustomLabelKeys(keys) }, runWithLabels (labels, fn) { return profilerModule.runWithLabels(labels, fn) }, } Reflect.defineProperty(this, 'profiling', { value: profiling, configurable: true, enumerable: true }) return profiling } /** * @override */ profilerStarted () { if (this._profilerStarted === undefined) { // injection hardening: this is only ever invoked from tests. throw new Error('profilerStarted() must be called after init()') } return Promise.resolve(this._profilerStarted) } /** * @override */ use () { this._pluginManager.configurePlugin(...arguments) return this } /** * @override */ get TracerProvider () { return require('./opentelemetry/tracer_provider') } } /** * Checks the private filesystem-only Test Optimization validation mode. * * @returns {boolean} whether network-capable tracer side channels must stay disabled */ function isOfflineTestOptimizationValidation () { return isTrue(getEnvironmentVariable(VALIDATION_MODE_ENV)) && Boolean(getEnvironmentVariable(VALIDATION_MANIFEST_ENV)) && Boolean(getEnvironmentVariable(VALIDATION_OUTPUT_ENV)) } /** * Checks whether initialization selected a filesystem-only Test Optimization exporter. * * @param {import('../../../index').TracerOptions} [options] tracer initialization options * @returns {boolean} whether the selected exporter is safe for offline validation */ function isOfflineValidationExporter (options) { return OFFLINE_VALIDATION_EXPORTERS.has(options?.experimental?.exporter) } module.exports = Tracer