UNPKG

lightstep-tracer

Version:

> ❗ **This instrumentation is no longer recommended**. Please review [documentation on setting up and configuring the OpenTelemetry Node.js Launcher](https://github.com/lightstep/otel-launcher-node) or [OpenTelemetry JS (Browser)](https://github.com/open-

295 lines (262 loc) 10.6 kB
// eslint-disable-next-line import/no-import-module-exports import * as opentracing from 'opentracing'; // eslint-disable-next-line import/no-import-module-exports import http from 'http'; // eslint-disable-next-line import/no-import-module-exports import https from 'https'; // eslint-disable-next-line import/no-import-module-exports import urlCreator, { URL } from 'url'; // Capture the proxied values on script load (i.e. ASAP) in case there are // multiple layers of instrumentation. let proxiedHttpRequest; let proxiedHttpsRequest; let proxiedHttpGet; let proxiedHttpsGet; if (typeof window === 'undefined') { proxiedHttpRequest = http.request; proxiedHttpGet = http.get; proxiedHttpsRequest = https.request; proxiedHttpsGet = https.get; } // taken from following // https://github.com/nodejs/node/blob/8507485fb242dfcaf07092414871aa9c185a28e4/lib/internal/url.js#L1254-L1276 // Utility function that converts a URL object into an ordinary // options object as expected by the http.request and https.request // APIs. function urlToOptions(url) { const options = { protocol : url.protocol, hostname : typeof url.hostname === 'string' && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname, hash : url.hash, search : url.search, pathname : url.pathname, path : `${url.pathname || ''}${url.search || ''}`, href : url.href, }; if (url.port !== '') { options.port = Number(url.port); } if (url.username || url.password) { options.auth = `${url.username}:${url.password}`; } return options; } // Automatically create spans for all requests made via window.fetch. // // NOTE: this code currently works only with a single Tracer. // class InstrumentNodejs { constructor() { this._enabled = this._isValidContext(); this._proxyInited = false; this._internalExclusions = []; this._tracer = null; this._handleOptions = this._handleOptions.bind(this); } name() { return 'instrument_nodejs'; } addOptions(tracerImp) { tracerImp.addOption('nodejs_instrumentation', { type : 'bool', defaultValue : false }); tracerImp.addOption('nodejs_url_inclusion_patterns', { type : 'array', defaultValue : [/.*/] }); tracerImp.addOption('nodejs_url_exclusion_patterns', { type : 'array', defaultValue : [] }); tracerImp.addOption('include_cookies', { type : 'bool', defaultValue : true }); } start(tracerImp) { if (!this._enabled) { return; } this._tracer = tracerImp; let currentOptions = tracerImp.options(); this._addServiceHostToExclusions(currentOptions); this._handleOptions({}, currentOptions); tracerImp.on('options', this._handleOptions); } stop() { if (!this._enabled) { return; } http.request = proxiedHttpRequest; http.get = proxiedHttpGet; https.request = proxiedHttpsRequest; https.get = proxiedHttpsGet; } /** * Respond to options changes on the Tracer. * * Note that `modified` is the options that have changed in this call, * along with their previous and new values. `current` is the full set of * current options *including* the newly modified values. */ _handleOptions(modified, current) { // Automatically add the service host itself to the list of exclusions // to avoid reporting on the reports themselves let serviceHost = modified.collector_host; if (serviceHost) { this._addServiceHostToExclusions(current); } // Set up the proxied fetch calls unless disabled if (!this._proxyInited && current.nodejs_instrumentation) { this._proxyInited = true; this._instrumentNodejs(); } } /** * Ensure that the reports to the collector don't get instrumented as well, * as that recursive instrumentation is more confusing than valuable! */ _addServiceHostToExclusions(opts) { if (opts.collector_host.length === 0) { return; } // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex function escapeRegExp(str) { return (`${str}`).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Check against the hostname without the port as well as the canonicalized // URL may drop the standard port. let host = escapeRegExp(opts.collector_host); let port = escapeRegExp(opts.collector_port); let set = [new RegExp(`^https?://${host}:${port}`)]; if (port === '80') { set.push(new RegExp(`^http://${host}`)); } else if (port === '443') { set.push(new RegExp(`^https://${host}`)); } this._internalExclusions = set; } /** * Check if in node */ _isValidContext() { const isNode = (typeof process !== 'undefined') && (typeof process.release !== 'undefined') && (process.release.name === 'node'); return isNode; } _instrumentNodejs() { let self = this; let tracer = this._tracer; function requestOverride(originalRequest, ...args) { // http.request has two overrides, taking url/string first, or options // if url or string morph into an options object, // make it so that options and possible callback are only args passed let options; let callback; let urlObject; /* eslint-disable prefer-destructuring */ if (typeof args[0] === 'string' || args[0] instanceof URL) { urlObject = args[0] instanceof URL ? args[0] : new URL(args[0]); options = urlToOptions(urlObject); if (typeof args[1] === 'object') { options = { ...options, ...args[1] }; callback = args[2]; } else if (typeof args[1] === 'function') { callback = args[1]; } } else { options = args[0]; callback = args[1]; } /* eslint-enable prefer-destructuring */ // check if there are headers stated, and if not create them on the first arg // then grab reference so that we can inject headers into the request before sending the request out if (!options.headers) options.headers = {}; const { headers } = options; const method = options.method || 'GET'; const url = options.href || urlCreator.format(options); const protocol = options.protocol ? options.protocol.replace(':', '') : url.slice(0, url.indexOf(':')); if (!self._shouldTrace(tracer, url)) { return originalRequest(...args); } const span = tracer.startSpan('node request'); tracer.addActiveRootSpan(span); let tags = { method : method || 'GET', url : url, protocol : protocol, }; if (url) { // eslint-disable-next-line prefer-destructuring tags.url_pathname = url.split('?')[0]; } try { const headersCarrier = {}; tracer.inject(span.context(), opentracing.FORMAT_HTTP_HEADERS, headersCarrier); const keys = Object.keys(headersCarrier); // add tracing headers to request // have to set headers instead of modifying the request instance headers, // In an http.get call case, req.end will automatically be called, // setting headers will be impossible after that point // reference https://nodejs.org/api/http.html#http_class_http_clientrequest keys.forEach((key) => { headers[key] = headersCarrier[key]; }); const request = originalRequest(options, callback); span.log({ event : 'sending', method : method || 'GET', url : url, openPayload : tags, }); span.addTags(tags); request.on('response', (res) => { if (res.statusCode >= 500 && res.statusCode <= 599) { span.addTags({ error : true }); } span.log({ method : method || 'GET', headers : res.headers, status : res.status, statusText : res.statusText, responseType : res.type, url : res.url, }); span.finish(); tracer.removeActiveRootSpan(span); }); return request; } catch (e) { span.addTags({ error : true }); tracer.removeActiveRootSpan(span); span.log({ event : 'error', error : e, }); span.finish(); throw e; } } http.request = requestOverride.bind(undefined, http.request); https.request = requestOverride.bind(undefined, https.request); http.get = requestOverride.bind(undefined, http.get); https.get = requestOverride.bind(undefined, https.get); } _shouldTrace(tracer, url) { // This shouldn't be possible, but let's be paranoid if (!tracer || !url) { return false; } let opts = tracer.options(); if (opts.disabled || !opts.nodejs_instrumentation) { return false; } if (this._internalExclusions.some((ex) => ex.test(url))) { return false; } let include = false; if (opts.nodejs_url_inclusion_patterns.some((inc) => inc.test(url))) { include = true; } if (opts.nodejs_url_exclusion_patterns.some((ex) => ex.test(url))) { include = false; } return include; } } module.exports = new InstrumentNodejs();