dd-trace
Version:
Datadog APM tracing client for JavaScript
202 lines (179 loc) • 5.75 kB
JavaScript
'use strict'
const { getValueFromEnvSources } = require('../../config/helper')
const log = require('../../log')
const request = require('../common/request')
const tracerVersion = require('../../../../../package.json').version
const BaseWriter = require('../common/writer')
const { AgentlessJSONEncoder } = require('../../encode/agentless-json')
/**
* Writer for agentless APM trace intake.
* Sends traces directly to the Datadog intake endpoint without an agent.
*/
class AgentlessWriter extends BaseWriter {
#apiKeyMissing = false
#urlMissing = false
/**
* @param {object} options - Writer options
* @param {URL} [options.url] - The intake URL. If not provided, constructed from site.
* @param {string} [options.site='datadoghq.com'] - The Datadog site
* @param {object} [options.metadata={}] - Metadata to pass to the encoder (hostname, env, etc.)
*/
constructor ({ url, site = 'datadoghq.com', metadata = {} }) {
super({ url })
this._encoder = new AgentlessJSONEncoder(this, metadata)
if (!url) {
try {
this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
} catch (err) {
log.error(
'Invalid site value for agentless intake: %s. Cannot construct URL. Error: %s',
site,
err.message
)
this._url = null
}
}
if (!getValueFromEnvSources('DD_API_KEY')) {
this.#apiKeyMissing = true
log.error('DD_API_KEY is required for agentless trace intake. Set DD_API_KEY. Traces will not be sent.')
}
}
setUrl (url) {
super.setUrl(url)
if (url) {
this.#urlMissing = false
}
}
/**
* Flushes accumulated traces to the intake as a single request.
* @param {Function} [done] - Callback when send completes
*/
flush (done = () => {}) {
if (!request.writable) {
const count = this._encoder.count()
if (count > 0) {
log.error('Maximum number of active requests reached. Dropping %d trace(s).', count)
}
this._encoder.reset()
done()
return
}
const count = this._encoder.count()
if (count === 0) {
done()
return
}
const payload = this._encoder.makePayload()
if (payload.length === 0) {
log.debug('Skipping send of empty payload')
done()
return
}
this._sendPayload(payload, count, done)
}
/**
* Sends the encoded payload to the intake endpoint.
* @param {Buffer} data - The encoded JSON payload
* @param {number} count - Number of traces in the payload
* @param {Function} done - Callback when complete
*/
_sendPayload (data, count, done) {
if (!data || data.length === 0) {
log.debug('Skipping send of empty payload')
done()
return
}
if (!this._url) {
if (!this.#urlMissing) {
this.#urlMissing = true
log.error('No valid URL configured for agentless trace intake. Traces will not be sent.')
}
log.debug('Dropping %d trace(s) due to missing URL', count)
done()
return
}
const apiKey = getValueFromEnvSources('DD_API_KEY')
if (!apiKey) {
if (!this.#apiKeyMissing) {
this.#apiKeyMissing = true
log.error('DD_API_KEY is required for agentless trace intake. Set DD_API_KEY. Traces will not be sent.')
}
log.debug('Dropping %d trace(s) due to missing DD_API_KEY', count)
done()
return
}
this.#apiKeyMissing = false
const options = {
path: '/v1/input',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'dd-api-key': apiKey,
'X-Datadog-Trace-Count': String(count),
'Datadog-Meta-Lang': 'nodejs',
'Datadog-Meta-Lang-Version': process.version,
'Datadog-Meta-Lang-Interpreter': process.versions.bun ? 'JavaScriptCore' : 'v8',
'Datadog-Meta-Tracer-Version': tracerVersion,
},
timeout: 15_000,
url: this._url,
}
log.debug('Request to the agentless intake: %j', options)
request(data, options, (err, res, statusCode) => {
if (err) {
this._logRequestError(err, statusCode, count)
done()
return
}
log.debug('Response from the agentless intake: %s', res)
done()
})
}
/**
* Logs request errors with status-specific guidance.
* @param {Error} err - The error object
* @param {number} statusCode - HTTP status code (if available)
* @param {number} count - Number of traces that were being sent
*/
_logRequestError (err, statusCode, count) {
if (statusCode === 401 || statusCode === 403) {
log.error(
'Authentication failed sending %d trace(s) (status %s). Verify DD_API_KEY is valid.',
count,
statusCode
)
} else if (statusCode === 404) {
log.error(
'Trace intake endpoint not found (status %s). Verify DD_SITE is correctly configured. %d trace(s) dropped.',
statusCode,
count
)
} else if (statusCode === 429) {
log.error(
'Rate limited by trace intake (status 429). %d trace(s) dropped.',
count
)
} else if (statusCode >= 500) {
log.error(
'Trace intake server error (status %s). %d trace(s) dropped. This may be transient.',
statusCode,
count
)
} else if (statusCode) {
log.error(
'Error sending agentless payload (status %s): %s. %d trace(s) dropped.',
statusCode,
err.message,
count
)
} else {
log.error(
'Network error sending %d trace(s) to %s: %s',
count,
this._url?.hostname || 'unknown',
err.message
)
}
}
}
module.exports = AgentlessWriter