UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

171 lines (135 loc) 4.77 kB
'use strict' // TODO: Add test with slow or unresponsive agent. // TODO: Add telemetry for things like dropped requests, errors, etc. const { Readable } = require('stream') const http = require('http') const https = require('https') const zlib = require('zlib') const { storage } = require('../../../../datadog-core') const log = require('../../log') const { urlToHttpOptions } = require('./url-to-http-options-polyfill') const docker = require('./docker') const { httpAgent, httpsAgent } = require('./agents') const maxActiveRequests = 8 let activeRequests = 0 function parseUrl (urlObjOrString) { if (urlObjOrString !== null && typeof urlObjOrString === 'object') return urlToHttpOptions(urlObjOrString) const url = urlToHttpOptions(new URL(urlObjOrString)) // Special handling if we're using named pipes on Windows if (url.protocol === 'unix:' && url.hostname === '.') { const udsPath = urlObjOrString.slice(5) url.path = udsPath url.pathname = udsPath } return url } function request (data, options, callback) { if (!options.headers) { options.headers = {} } if (options.url) { const url = parseUrl(options.url) if (url.protocol === 'unix:') { options.socketPath = url.pathname } else { if (!options.path) options.path = url.path options.protocol = url.protocol options.hostname = url.hostname // for IPv6 this should be '::1' and not '[::1]' options.port = url.port } } const isReadable = data instanceof Readable // The timeout should be kept low to avoid excessive queueing. const timeout = options.timeout || 2000 const isSecure = options.protocol === 'https:' const client = isSecure ? https : http let dataArray = data if (!isReadable) { if (!Array.isArray(data)) { dataArray = [data] } options.headers['Content-Length'] = byteLength(dataArray) } docker.inject(options.headers) options.agent = isSecure ? httpsAgent : httpAgent const onResponse = res => { const chunks = [] res.setTimeout(timeout) res.on('data', chunk => { chunks.push(chunk) }) res.on('end', () => { activeRequests-- const buffer = Buffer.concat(chunks) if (res.statusCode >= 200 && res.statusCode <= 299) { const isGzip = res.headers['content-encoding'] === 'gzip' if (isGzip) { zlib.gunzip(buffer, (err, result) => { if (err) { log.error('Could not gunzip response: %s', err.message) callback(null, '', res.statusCode) } else { callback(null, result.toString(), res.statusCode) } }) } else { callback(null, buffer.toString(), res.statusCode) } } else { let errorMessage = '' try { const fullUrl = new URL( options.path, options.url || options.hostname || `http://localhost:${options.port}` ).href errorMessage = `Error from ${fullUrl}: ${res.statusCode} ${http.STATUS_CODES[res.statusCode]}.` } catch { // ignore error } const responseData = buffer.toString() if (responseData) { errorMessage += ` Response from the endpoint: "${responseData}"` } const error = new log.NoTransmitError(errorMessage) error.status = res.statusCode callback(error, null, res.statusCode) } }) } const makeRequest = onError => { if (!request.writable) { log.debug('Maximum number of active requests reached: payload is discarded.') return callback(null) } activeRequests++ const store = storage('legacy').getStore() storage('legacy').enterWith({ noop: true }) const req = client.request(options, onResponse) req.once('error', err => { activeRequests-- onError(err) }) req.setTimeout(timeout, req.abort) if (isReadable) { data.pipe(req) // TODO: Validate whether this is actually retriable. } else { dataArray.forEach(buffer => req.write(buffer)) req.end() } storage('legacy').enterWith(store) } // TODO: Figure out why setTimeout is needed to avoid losing the async context // in the retry request before socket.connect() is called. // TODO: Test that this doesn't trace itself on retry when the diagnostics // channel events are available in the agent exporter. makeRequest(() => setTimeout(() => makeRequest(callback))) } function byteLength (data) { return data.length > 0 ? data.reduce((prev, next) => prev + Buffer.byteLength(next, 'utf8'), 0) : 0 } Object.defineProperty(request, 'writable', { get () { return activeRequests < maxActiveRequests } }) module.exports = request