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-

282 lines (250 loc) 9.33 kB
// eslint-disable-next-line import/no-import-module-exports import * as opentracing from 'opentracing'; // Capture the proxied values on script load (i.e. ASAP) in case there are // multiple layers of instrumentation. let proxiedFetch; if (typeof window === 'object' && typeof window.fetch !== 'undefined') { proxiedFetch = window.fetch; } function getCookies() { if (typeof document === 'undefined' || !document.cookie) { return null; } let cookies = document.cookie.split(';'); let data = {}; let count = 0; for (let i = 0; i < cookies.length; i++) { let parts = cookies[i].split('=', 2); if (parts.length === 2) { let key = parts[0].replace(/^\s+/, '').replace(/\s+$/, ''); data[key] = decodeURIComponent(parts[1]); try { data[key] = JSON.parse(data[key]); } catch (_ignored) { /* Ignored */ } count++; } } if (count > 0) { return data; } return null; } // Normalize the getAllResponseHeaders output function getResponseHeaders(response) { const result = {}; const entries = response.headers.entries(); for (let i = 0; i < entries.length; i++) { const pair = entries[i]; const [key, val] = pair; result[key] = val; } return result; } // Automatically create spans for all requests made via window.fetch. // // NOTE: this code currently works only with a single Tracer. // class InstrumentFetch { constructor() { this._enabled = this._isValidContext(); this._proxyInited = false; this._internalExclusions = []; this._tracer = null; this._handleOptions = this._handleOptions.bind(this); } name() { return 'instrument_fetch'; } addOptions(tracerImp) { tracerImp.addOption('fetch_instrumentation', { type : 'bool', defaultValue : false }); tracerImp.addOption('fetch_url_inclusion_patterns', { type : 'array', defaultValue : [/.*/] }); tracerImp.addOption('fetch_url_exclusion_patterns', { type : 'array', defaultValue : [] }); tracerImp.addOption('fetch_url_header_inclusion_patterns', { type : 'array', defaultValue : [/.*/] }); tracerImp.addOption('fetch_url_header_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; } window.fetch = proxiedFetch; } /** * 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.fetch_instrumentation) { this._proxyInited = true; window.fetch = this._instrumentFetch(); } } /** * 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 preconditions for the auto-instrumentation of fetch to work properly. * There are a lot of potential JavaScript platforms. */ _isValidContext() { if (typeof window === 'undefined') { return false; } if (!window.fetch) { return false; } return true; } _instrumentFetch() { let self = this; let tracer = this._tracer; return function (input, init) { const request = new Request(input, init); const opts = tracer.options(); if (!self._shouldTrace(tracer, request.url)) { // eslint-disable-next-line prefer-spread return proxiedFetch(request); } let span = tracer.startSpan('fetch'); tracer.addActiveRootSpan(span); const parsed = new URL(request.url); let tags = { method : request.method, url : request.url, // NOTE: Purposefully excluding username:password from tags. // TODO: consider sanitizing URL to mask / remove that information from the trace in general hash : parsed.hash, href : parsed.href, protocol : parsed.protocol, origin : parsed.origin, host : parsed.host, hostname : parsed.hostname, port : parsed.port, pathname : parsed.pathname, search : parsed.search, }; if (opts.include_cookies) { tags.cookies = getCookies(); } // Add Open-Tracing headers if (self._shouldAddHeadersToRequest(tracer, request.url)) { const headersCarrier = {}; tracer.inject(span.context(), opentracing.FORMAT_HTTP_HEADERS, headersCarrier); Object.keys(headersCarrier).forEach((key) => { if (!request.headers.get(key)) request.headers.set(key, headersCarrier[key]); }); } span.log({ event : 'sending', method : request.method, url : request.url, openPayload : tags, }); span.addTags(tags); return proxiedFetch(request).then((response) => { if (!response.ok) { span.addTags({ error : true }); } span.log({ method : request.method, headers : getResponseHeaders(response), status : response.status, statusText : response.statusText, responseType : response.type, url : response.url, }); tracer.removeActiveRootSpan(span); span.finish(); return response; }).catch((e) => { span.addTags({ error : true }); tracer.removeActiveRootSpan(span); span.log({ event : 'error', error : e, }); span.finish(); throw e; }); }; } _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) { return false; } if (this._internalExclusions.some((ex) => ex.test(url))) { return false; } if (opts.fetch_url_exclusion_patterns.some((ex) => ex.test(url))) { return false; } if (opts.fetch_url_inclusion_patterns.some((inc) => inc.test(url))) { return true; } return false; } _shouldAddHeadersToRequest(tracer, url) { // This shouldn't be possible, but let's be paranoid if (!tracer || !url) { return false; } let opts = tracer.options(); if (opts.disabled) { return false; } if (opts.fetch_url_header_exclusion_patterns.some((ex) => ex.test(url))) { return false; } if (opts.fetch_url_header_inclusion_patterns.some((inc) => inc.test(url))) { return true; } return false; } } module.exports = new InstrumentFetch();