dd-trace
Version:
Datadog APM tracing client for JavaScript
481 lines (417 loc) • 16.2 kB
JavaScript
'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