dd-trace
Version:
Datadog APM tracing client for JavaScript
391 lines (335 loc) • 11 kB
JavaScript
const tracerVersion = require('../../../../package.json').version
const dc = require('dc-polyfill')
const os = require('os')
const dependencies = require('./dependencies')
const endpoints = require('./endpoints')
const { sendData } = require('./send-data')
const { errors } = require('../startup-log')
const { manager: metricsManager } = require('./metrics')
const telemetryLogger = require('./logs')
const logger = require('../log')
const telemetryStartChannel = dc.channel('datadog:telemetry:start')
const telemetryStopChannel = dc.channel('datadog:telemetry:stop')
const telemetryAppClosingChannel = dc.channel('datadog:telemetry:app-closing')
let config
let pluginManager
let application
let host
let heartbeatTimeout
let heartbeatInterval
let extendedInterval
let integrations
const configWithOrigin = new Map()
let retryData = null
const extendedHeartbeatPayload = {}
const sentIntegrations = new Set()
let seqId = 0
function getRetryData () {
return retryData
}
function updateRetryData (error, retryObj) {
if (error) {
if (retryObj.reqType === 'message-batch') {
const payload = retryObj.payload[0].payload
const reqType = retryObj.payload[0].request_type
retryData = { payload, reqType }
// Since this payload failed twice it now gets save in to the extended heartbeat
const failedPayload = retryObj.payload[1].payload
const failedReqType = retryObj.payload[1].request_type
// save away the dependencies and integration request for extended heartbeat.
if (failedReqType === 'app-integrations-change') {
if (extendedHeartbeatPayload.integrations) {
extendedHeartbeatPayload.integrations.push(failedPayload)
} else {
extendedHeartbeatPayload.integrations = [failedPayload]
}
}
if (failedReqType === 'app-dependencies-loaded') {
if (extendedHeartbeatPayload.dependencies) {
extendedHeartbeatPayload.dependencies.push(failedPayload)
} else {
extendedHeartbeatPayload.dependencies = [failedPayload]
}
}
} else {
retryData = retryObj
}
} else {
retryData = null
}
}
function getIntegrations () {
const newIntegrations = []
for (const pluginName in pluginManager._pluginsByName) {
if (sentIntegrations.has(pluginName)) {
continue
}
newIntegrations.push({
name: pluginName,
enabled: pluginManager._pluginsByName[pluginName]._enabled,
auto_enabled: true
})
sentIntegrations.add(pluginName)
}
return newIntegrations
}
function getProducts (config) {
const products = {
appsec: {
enabled: config.appsec.enabled
},
profiler: {
version: tracerVersion,
enabled: profilingEnabledToBoolean(config.profiling.enabled)
}
}
if (errors.profilingError) {
products.profiler.error = errors.profilingError
errors.profilingError = {}
}
return products
}
function getInstallSignature (config) {
const { installSignature: sig } = config
if (sig && (sig.id || sig.time || sig.type)) {
return {
install_id: sig.id,
install_time: sig.time,
install_type: sig.type
}
}
}
function appStarted (config) {
const app = {
products: getProducts(config),
configuration: [...configWithOrigin.values()]
}
const installSignature = getInstallSignature(config)
if (installSignature) {
app.install_signature = installSignature
}
// TODO: add app.error with correct error codes
// if (errors.agentError) {
// app.error = errors.agentError
// errors.agentError = {}
// }
return app
}
function appClosing () {
if (!config?.telemetry?.enabled) {
return
}
// Give chance to listeners to update metrics before shutting down.
telemetryAppClosingChannel.publish()
const { reqType, payload } = createPayload('app-closing')
sendData(config, application, host, reqType, payload)
// We flush before shutting down.
metricsManager.send(config, application, host)
telemetryLogger.send(config, application, host)
}
function onBeforeExit () {
process.removeListener('beforeExit', onBeforeExit)
appClosing()
}
function createAppObject (config) {
return {
service_name: config.service,
env: config.env,
service_version: config.version,
tracer_version: tracerVersion,
language_name: 'nodejs',
language_version: process.versions.node
}
}
function createHostObject () {
const osName = os.type()
if (osName === 'Linux' || osName === 'Darwin') {
return {
hostname: os.hostname(),
os: osName,
architecture: os.arch(),
kernel_version: os.version(),
kernel_release: os.release(),
kernel_name: osName
}
}
if (osName === 'Windows_NT') {
return {
hostname: os.hostname(),
os: osName,
architecture: os.arch(),
os_version: os.version()
}
}
return {
hostname: os.hostname(), // TODO is this enough?
os: osName
}
}
function getTelemetryData () {
return { config, application, host, heartbeatInterval }
}
function createBatchPayload (payload) {
const batchPayload = payload.map(item => {
return {
request_type: item.reqType,
payload: item.payload
}
})
return batchPayload
}
function createPayload (currReqType, currPayload = {}) {
if (getRetryData()) {
const payload = { reqType: currReqType, payload: currPayload }
const batchPayload = createBatchPayload([payload, retryData])
return { reqType: 'message-batch', payload: batchPayload }
}
return { reqType: currReqType, payload: currPayload }
}
function heartbeat (config, application, host) {
heartbeatTimeout = setTimeout(() => {
metricsManager.send(config, application, host)
telemetryLogger.send(config, application, host)
const { reqType, payload } = createPayload('app-heartbeat')
sendData(config, application, host, reqType, payload, updateRetryData)
heartbeat(config, application, host)
}, heartbeatInterval).unref()
return heartbeatTimeout
}
function extendedHeartbeat (config) {
extendedInterval = setInterval(() => {
const appPayload = appStarted(config)
const payload = {
...appPayload,
...extendedHeartbeatPayload
}
sendData(config, application, host, 'app-extended-heartbeat', payload)
Object.keys(extendedHeartbeatPayload).forEach(key => delete extendedHeartbeatPayload[key])
}, 1000 * 60 * 60 * 24).unref()
return extendedInterval
}
function start (aConfig, thePluginManager) {
if (!aConfig.telemetry.enabled) {
if (aConfig.sca?.enabled) {
logger.warn('DD_APPSEC_SCA_ENABLED requires enabling telemetry to work.')
}
return
}
config = aConfig
pluginManager = thePluginManager
application = createAppObject(config)
host = createHostObject()
heartbeatInterval = config.telemetry.heartbeatInterval
integrations = getIntegrations()
dependencies.start(config, application, host, getRetryData, updateRetryData)
telemetryLogger.start(config)
endpoints.start(config, application, host, getRetryData, updateRetryData)
sendData(config, application, host, 'app-started', appStarted(config))
if (integrations.length > 0) {
sendData(config, application, host, 'app-integrations-change',
{ integrations }, updateRetryData)
}
heartbeat(config, application, host)
extendedHeartbeat(config)
process.on('beforeExit', onBeforeExit)
telemetryStartChannel.publish(getTelemetryData())
}
function stop () {
if (!config) {
return
}
clearInterval(extendedInterval)
clearTimeout(heartbeatTimeout)
process.removeListener('beforeExit', onBeforeExit)
telemetryStopChannel.publish(getTelemetryData())
endpoints.stop()
config = undefined
}
function updateIntegrations () {
if (!config?.telemetry.enabled) {
return
}
const integrations = getIntegrations()
if (integrations.length === 0) {
return
}
const { reqType, payload } = createPayload('app-integrations-change', { integrations })
sendData(config, application, host, reqType, payload, updateRetryData)
}
function formatMapForTelemetry (map) {
// format from an object to a string map in order for
// telemetry intake to accept the configuration
return map
? Object.entries(map).map(([key, value]) => `${key}:${value}`).join(',')
: ''
}
const nameMapping = {
sampleRate: 'DD_TRACE_SAMPLE_RATE',
logInjection: 'DD_LOG_INJECTION',
headerTags: 'DD_TRACE_HEADER_TAGS',
tags: 'DD_TAGS',
'sampler.rules': 'DD_TRACE_SAMPLING_RULES',
traceEnabled: 'DD_TRACE_ENABLED',
url: 'DD_TRACE_AGENT_URL',
'sampler.rateLimit': 'DD_TRACE_RATE_LIMIT',
queryStringObfuscation: 'DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP',
version: 'DD_VERSION',
env: 'DD_ENV',
service: 'DD_SERVICE',
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',
instrumentationSource: 'instrumentation_source',
injectionEnabled: 'ssi_injection_enabled',
injectForce: 'ssi_forced_injection_enabled',
'runtimeMetrics.enabled': 'runtimeMetrics'
}
const namesNeedFormatting = new Set(['DD_TAGS', 'peerServiceMapping', 'serviceMapping'])
function updateConfig (changes, config) {
if (!config.telemetry.enabled) return
if (changes.length === 0) return
logger.trace(changes)
const application = createAppObject(config)
const host = createHostObject()
const changed = configWithOrigin.size > 0
for (const change of changes) {
const name = nameMapping[change.name] || change.name
const { origin, value } = change
const entry = { name, value, origin, seq_id: seqId++ }
if (namesNeedFormatting.has(entry.name)) {
entry.value = formatMapForTelemetry(entry.value)
} else if (entry.name === 'url') {
if (entry.value) {
entry.value = entry.value.toString()
}
} else if (entry.name === 'DD_TRACE_SAMPLING_RULES') {
entry.value = JSON.stringify(entry.value)
} else if (Array.isArray(entry.value)) {
entry.value = value.join(',')
}
// Use composite key to support multiple origins for same config name
configWithOrigin.set(`${name}|${origin}`, entry)
}
if (changed) {
// update configWithOrigin to contain up-to-date full list of config values for app-extended-heartbeat
const { reqType, payload } = createPayload('app-client-configuration-change', {
configuration: [...configWithOrigin.values()]
})
sendData(config, application, host, reqType, payload, updateRetryData)
}
}
function profilingEnabledToBoolean (profilingEnabled) {
if (typeof profilingEnabled === 'boolean') {
return profilingEnabled
}
return profilingEnabled === 'true' || profilingEnabled === 'auto'
}
module.exports = {
start,
stop,
updateIntegrations,
updateConfig,
appClosing
}