UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

201 lines (171 loc) 6.17 kB
'use strict' const { request: httpRequest } = require('http') const { request: httpsRequest } = require('https') const perf = require('perf_hooks').performance const { urlToHttpOptions } = require('url') const retry = require('../../../../../vendor/dist/retry') // TODO: avoid using dd-trace internals. Make this a separate module? const docker = require('../../exporters/common/docker') const FormData = require('../../exporters/common/form-data') const { storage } = require('../../../../datadog-core') const version = require('../../../../../package.json').version const telemetryMetrics = require('../../telemetry/metrics') const { EventSerializer } = require('./event_serializer') const profilersNamespace = telemetryMetrics.manager.namespace('profilers') const statusCodeCounters = [] const requestCounter = profilersNamespace.count('profile_api.requests', []) const sizeDistribution = profilersNamespace.distribution('profile_api.bytes', []) const durationDistribution = profilersNamespace.distribution('profile_api.ms', []) const statusCodeErrorCounter = profilersNamespace.count('profile_api.errors', ['type:status_code']) const networkErrorCounter = profilersNamespace.count('profile_api.errors', ['type:network']) // TODO: implement timeout error counter when we have a way to track timeouts // const timeoutErrorCounter = profilersNamespace.count('profile_api.errors', ['type:timeout']) function countStatusCode (statusCode) { let counter = statusCodeCounters[statusCode] if (counter === undefined) { counter = statusCodeCounters[statusCode] = profilersNamespace.count( 'profile_api.responses', [`status_code:${statusCode}`] ) } counter.inc() } function sendRequest (options, form, callback) { const request = options.protocol === 'https:' ? httpsRequest : httpRequest const store = storage('legacy').getStore() storage('legacy').enterWith({ noop: true }) requestCounter.inc() const start = perf.now() const req = request(options, res => { durationDistribution.track(perf.now() - start) countStatusCode(res.statusCode) if (res.statusCode >= 400) { statusCodeErrorCounter.inc() const error = new Error(`HTTP Error ${res.statusCode}`) error.status = res.statusCode callback(error) } else { callback(null, res) } }) req.on('error', (err) => { networkErrorCounter.inc() callback(err) }) if (form) { sizeDistribution.track(form.size()) form.pipe(req) } storage('legacy').enterWith(store) } function getBody (stream, callback) { const chunks = [] stream.on('error', (err) => { networkErrorCounter.inc() callback(err) }) stream.on('data', chunk => chunks.push(chunk)) stream.on('end', () => { callback(null, Buffer.concat(chunks)) }) } function computeRetries (uploadTimeout) { let tries = 0 while (tries < 2 || uploadTimeout > 1000) { tries++ uploadTimeout /= 2 } return [tries, Math.floor(uploadTimeout)] } class AgentExporter extends EventSerializer { constructor (config = {}) { super(config) const { url, logger, uploadTimeout } = config this._url = url this._logger = logger const [backoffTries, backoffTime] = computeRetries(uploadTimeout) this._backoffTime = backoffTime this._backoffTries = backoffTries } export (exportSpec) { const { profiles } = exportSpec const fields = [] const event = this.getEventJSON(exportSpec) fields.push(['event', event, { filename: 'event.json', contentType: 'application/json' }]) this._logger.debug(() => { return `Building agent export report:\n${event}` }) for (const [type, buffer] of Object.entries(profiles)) { this._logger.debug(() => { const bytes = buffer.toString('hex').match(/../g).join(' ') return `Adding ${type} profile to agent export: ` + bytes }) const filename = this.typeToFile(type) fields.push([filename, buffer, { filename, contentType: 'application/octet-stream' }]) } return new Promise((resolve, reject) => { const operation = retry.operation({ randomize: true, minTimeout: this._backoffTime, retries: this._backoffTries, unref: true }) operation.attempt((attempt) => { const form = new FormData() for (const [key, value, options] of fields) { form.append(key, value, options) } const options = { method: 'POST', path: '/profiling/v1/input', headers: { 'DD-EVP-ORIGIN': 'dd-trace-js', 'DD-EVP-ORIGIN-VERSION': version, ...form.getHeaders() }, timeout: this._backoffTime * 2 ** attempt } docker.inject(options.headers) if (this._url.protocol === 'unix:') { options.socketPath = this._url.pathname } else { const httpOptions = urlToHttpOptions(this._url) options.protocol = httpOptions.protocol options.hostname = httpOptions.hostname options.port = httpOptions.port } this._logger.debug(() => { return `Submitting profiler agent report attempt #${attempt} to: ${JSON.stringify(options)}` }) sendRequest(options, form, (err, response) => { if (err) { const { status } = err if ((typeof status !== 'number' || status >= 500 || status === 429) && operation.retry(err)) { this._logger.warn(`Error from the agent: ${err.message}`) } else { reject(err) } return } getBody(response, (err, body) => { if (err) { this._logger.warn(`Error reading agent response: ${err.message}`) } else { this._logger.debug(() => { const bytes = (body.toString('hex').match(/../g) || []).join(' ') return `Agent export response: ${bytes}` }) } }) resolve() }) }) }) } } module.exports = { AgentExporter, computeRetries }