diff --git a/README.md b/README.md index 5f6085a..d3904a1 100755 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ BOOMR.init({ scheduledDelayMillis: 500, exportTimeoutMillis: 30000, }, + prototypeExporterPatch: true, // patches the OpenTelemetry collector-span-exporter in case the Prototype framework is used commonAttributes: { "application": "demo-app", "stage": "prod" @@ -132,28 +133,29 @@ BOOMR.init({ ``` Available options are: -| Option | Description | Default value | -|-------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| -| `samplingRate` | Sampling rate to use when collecting spans. Value must be between `0` and `1`. | `1` | -| `corsUrls` | Array of CORS URLs to take into consideration when propagating trace information. By default, CORS URLs are excluded from the propagation. | `[]` | -| `collectorConfiguration` | Object that defines the OpenTelemetry collector configuration, like the URL to send spans to. See [OTLPExporterNodeConfigBase](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-http) interface for all options. | `undefined` | -| `consoleOnly` | If `true` spans will be logged on the console and not sent to the collector endpoint. | `false` | -| `plugins` | Object for enabling and disabling OpenTelemetry plugins. | | -| `plugins.instrument_fetch` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch) for instrumentation of the fetch API. This will only be used in case the `fetch` API exists. | `true` | -| `plugins.instrument_xhr` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-xml-http-request) for instrumentation of the XMLHttpRequest API. | `true` | -| `plugins.instrument_document_load` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-document-load) for instrumentation of the document load (initial request). | `true` | -| `plugins.instrument_user_interaction` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction) for instrumentation of user interactions. | `true` | -| `plugins.browser_detector` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-browser-detector) for detecting browser attributes. | `true` | -| `global_instrumentation` | Object for configuring additional instrumentations, which will be applied to every OpenTelemetry plugin. | | -| `global_instrumentation.requestParameter` | If enabled, existing request parameters will be added as attributes to spans and, if not excluded, will be added to the corresponding beacon as well. | | -| `exporter` | Object for configuring the span exporter. Only used if `consoleOnly` is not enabled. | | -| `exporter.maxQueueSize` | The maximum queue size. After the size is reached spans are dropped. | `100` | -| `exporter.maxExportBatchSize` | The maximum batch size of every export. It must be smaller or equal to `maxQueueSize`. | `10` | -| `exporter.scheduledDelayMillis` | The interval between two consecutive exports. | `500` | -| `exporter.exportTimeoutMillis` | How long the export can run before it is cancelled. | `30000` | -| `commonAttributes` | An Object defining common span attributes which will be added to each recorded span. | `{}` | -| `serviceName` | A `string` or function which can be used to set the spans' service name. A function can be defined for dynamically providing the service name, e.g. based on Boomerang values. | `undefined` | -| `propagationHeader` | Defines the format of the context propagation header. Available formats: `TRACE_CONTEXT`, `B3_SINGLE`, `B3_MULTI` | `TRACE_CONTEXT` | +| Option | Description | Default value | +|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| `samplingRate` | Sampling rate to use when collecting spans. Value must be between `0` and `1`. | `1` | +| `corsUrls` | Array of CORS URLs to take into consideration when propagating trace information. By default, CORS URLs are excluded from the propagation. | `[]` | +| `collectorConfiguration` | Object that defines the OpenTelemetry collector configuration, like the URL to send spans to. See [OTLPExporterNodeConfigBase](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-http) interface for all options. | `undefined` | +| `consoleOnly` | If `true` spans will be logged on the console and not sent to the collector endpoint. | `false` | +| `plugins` | Object for enabling and disabling OpenTelemetry plugins. | | +| `plugins.instrument_fetch` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch) for instrumentation of the fetch API. This will only be used in case the `fetch` API exists. | `true` | +| `plugins.instrument_xhr` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-xml-http-request) for instrumentation of the XMLHttpRequest API. | `true` | +| `plugins.instrument_document_load` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-document-load) for instrumentation of the document load (initial request). | `true` | +| `plugins.instrument_user_interaction` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction) for instrumentation of user interactions. | `true` | +| `plugins.browser_detector` | Enabling the [OpenTelemetry plugin](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-browser-detector) for detecting browser attributes. | `true` | +| `global_instrumentation` | Object for configuring additional instrumentations, which will be applied to every OpenTelemetry plugin. | | +| `global_instrumentation.requestParameter` | If enabled, existing request parameters will be added as attributes to spans and, if not excluded, will be added to the corresponding beacon as well. | | +| `exporter` | Object for configuring the span exporter. Only used if `consoleOnly` is not enabled. | | +| `exporter.maxQueueSize` | The maximum queue size. After the size is reached spans are dropped. | `100` | +| `exporter.maxExportBatchSize` | The maximum batch size of every export. It must be smaller or equal to `maxQueueSize`. | `10` | +| `exporter.scheduledDelayMillis` | The interval between two consecutive exports. | `500` | +| `exporter.exportTimeoutMillis` | How long the export can run before it is cancelled. | `30000` | +| `prototypeExporterPatch` | Patches the OpenTelemetry collector-span-exporter, so it is compatible with the Prototype framework. This is only necessary and should only be activated, when the Prototype framework is used. [For more information see the linked file](https://github.com/NovatecConsulting/boomerang-opentelemetry-plugin/blob/master/src/impl/patchCollectorPrototype.ts). | `false` | +| `commonAttributes` | An Object defining common span attributes which will be added to each recorded span. | `{}` | +| `serviceName` | A `string` or function which can be used to set the spans' service name. A function can be defined for dynamically providing the service name, e.g. based on Boomerang values. | `undefined` | +| `propagationHeader` | Defines the format of the context propagation header. Available formats: `TRACE_CONTEXT`, `B3_SINGLE`, `B3_MULTI` | `TRACE_CONTEXT` | ## Manual Instrumentation diff --git a/src/impl/index.ts b/src/impl/index.ts index d4f7889..eae5745 100644 --- a/src/impl/index.ts +++ b/src/impl/index.ts @@ -32,6 +32,7 @@ import { CustomXMLHttpRequestInstrumentation } from './instrumentation/xmlHttpRe import { CustomFetchInstrumentation } from './instrumentation/fetchInstrumentation'; import { CustomUserInteractionInstrumentation } from './instrumentation/userInteractionInstrumentation'; import { browserDetector } from '@opentelemetry/opentelemetry-browser-detector'; +import { patchExporterClass } from './patchCollectorPrototype'; /** * TODOs: @@ -95,6 +96,7 @@ export default class OpenTelemetryTracingImpl { scheduledDelayMillis: 500, exportTimeoutMillis: 30000, }, + prototypeExporterPatch: true, commonAttributes: {}, serviceName: undefined, propagationHeader: PropagationHeader.TRACE_CONTEXT @@ -159,13 +161,10 @@ export default class OpenTelemetryTracingImpl { const exporter = new OTLPTraceExporter(collectorOptions); - // Patch is no longer necessary, since the new exporter does no longer use Array.from() - // TODO Remove patch after tested in production // patches the collector-export in order to be compatible with Prototype. - // if (this.props.prototypeExporterPatch) { - // patchExporter(exporter); - // patchExporterClass(); - // } + if (this.props.prototypeExporterPatch) { + patchExporterClass(); + } const batchSpanProcessor = new BatchSpanProcessor(exporter, { ...this.defaultProperties.exporter, diff --git a/src/impl/patchCollectorPrototype.ts b/src/impl/patchCollectorPrototype.ts index e7b28b6..bc4275d 100644 --- a/src/impl/patchCollectorPrototype.ts +++ b/src/impl/patchCollectorPrototype.ts @@ -1,217 +1,93 @@ -// /** -// * This file is basically a partial copy of the `https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-collector/src/transform.ts` file -// * of the OpenTelemetry Collector span exporter. We adapted some methods/functions in order to solve the following problems: -// * <> -// * The original exporter is using the `Array.from` method which is overridden by Prototype. The Prototype's function does not provide -// * all functionallity of the original function, thus, the exporter will fail exporting spans in case Prototype is used. -// * See: https://github.com/prototypejs/prototype/issues/338 -// * <> -// * The original exporter is using the `JSON.stringify` method. This method is calling `toJSON` functions on the object to serialize. -// * Unfortuently, prototype is adding a `toJSON` method to the Array class in versions prior 1.7. This leads to the problem, that nested -// * arrays are stringified seperatly, thus, they are considered not as an array anymore but as a string resulting in a invalid JSON string. -// * See: https://stackoverflow.com/questions/29637962/json-stringify-turned-the-value-array-into-a-string/29638420#29638420 -// * <> -// * In this file, a exporter can be patched using the `patchExporter` function, so the previously described problems are "solved". -// */ -// import { -// CollectorExporterConfigBase, -// opentelemetryProto, -// } from '@opentelemetry/exporter-collector/build/src/types'; -// import { SpanAttributes, diag } from '@opentelemetry/api'; -// import { -// CollectorExporterBase, -// CollectorTraceExporter, -// collectorTypes, -// } from '@opentelemetry/exporter-collector'; -// import * as core from '@opentelemetry/core'; -// import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -// import { Resource } from '@opentelemetry/resources'; -// import { -// groupSpansByResourceAndLibrary, -// toCollectorResource, -// toCollectorSpan, -// } from '@opentelemetry/exporter-collector/build/src/transform'; -// -// import { -// sendWithBeacon, -// sendWithXhr, -// } from '@opentelemetry/exporter-collector/build/src/platform/browser/util'; -// -// /** -// * -// * Returns a list of resource spans which will be exported to the collector. -// * @param groupedSpans -// * @param baseAttributes -// * @param useHex - if ids should be kept as hex without converting to base64 -// */ -// function toCollectorResourceSpans( -// groupedSpans: Map>, -// baseAttributes: SpanAttributes, -// useHex?: boolean -// ): opentelemetryProto.trace.v1.ResourceSpans[] { -// const resultSpans: Array = []; -// -// groupedSpans.forEach((libSpans, resource) => { -// const instLibSpans: Array = []; -// -// libSpans.forEach((spans, instrumentationLibrary) => { -// instLibSpans.push( -// toCollectorInstrumentationLibrarySpans( -// instrumentationLibrary, -// spans, -// useHex -// ) -// ); -// }); -// -// resultSpans.push({ -// resource: toCollectorResource(resource, baseAttributes), -// instrumentationLibrarySpans: instLibSpans, -// }); -// }); -// -// return resultSpans; -// } -// -// /** -// * ######################################## -// * # This function has NOT been modified. # -// * ######################################## -// * -// * Convert to InstrumentationLibrarySpans -// * @param instrumentationLibrary -// * @param spans -// * @param useHex - if ids should be kept as hex without converting to base64 -// */ -// function toCollectorInstrumentationLibrarySpans( -// instrumentationLibrary: core.InstrumentationLibrary, -// spans: ReadableSpan[], -// useHex?: boolean -// ): opentelemetryProto.trace.v1.InstrumentationLibrarySpans { -// return { -// spans: spans.map((span) => toCollectorSpan(span, useHex)), -// instrumentationLibrary, -// }; -// } -// -// /** -// * Prepares trace service request to be sent to collector -// * @param spans spans -// * @param collectorExporterBase -// * @param useHex - if ids should be kept as hex without converting to base64 -// */ -// function toCollectorExportTraceServiceRequest< -// T extends CollectorExporterConfigBase -// >( -// spans: ReadableSpan[], -// collectorTraceExporterBase: CollectorExporterBase< -// T, -// ReadableSpan, -// opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest -// >, -// useHex?: boolean -// ): opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { -// const groupedSpans: Map< -// Resource, -// Map -// > = groupSpansByResourceAndLibrary(spans); -// -// const additionalAttributes = Object.assign( -// {}, -// collectorTraceExporterBase.attributes -// ); -// -// return { -// resourceSpans: toCollectorResourceSpans( -// groupedSpans, -// additionalAttributes, -// useHex -// ), -// }; -// } -// -// /** -// * Patchs the given exporter. -// * -// * @param traceExporter the exporter instance to patch -// */ -// export function patchExporter(traceExporter: CollectorTraceExporter) { -// // Patches the transformation function, in order to call the custom `toCollectorExportTraceServiceRequest` -// // function to prevent calls of `Array.from`. -// traceExporter.convert = (spans) => { -// return toCollectorExportTraceServiceRequest(spans, traceExporter, true); -// }; -// } -// -// // ########################################################################################## -// // ########################################################################################## -// // ########################################################################################## -// // ########################################################################################## -// -// /** -// * This function is basically a copy of the `send` function of the following file: -// * https://github.com/open-telemetry/opentelemetry-js/blob/v0.25.0/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts -// * -// * Here, a "fix" has been added in order to support Prorotype prior 1.7. -// */ -// function sendPatch( -// items: any[], -// onSuccess: () => void, -// onError: (error: collectorTypes.CollectorExporterError) => void -// ) { -// if (this._isShutdown) { -// diag.debug('Shutdown already started. Cannot send objects'); -// return; -// } -// const serviceRequest = this.convert(items); -// -// // in order to fix the problem, we temporarly remove the `toJSON`` -// // function (1), serializing the spans (2) and readding the function (3) -// // in order to preserve the initial state of the class -// -// // (1) -// const arrayPrototype: any = Array.prototype; -// const arrayToJson = arrayPrototype.toJSON; -// delete arrayPrototype.toJSON; -// // (2) -// const body = JSON.stringify(serviceRequest); -// // (3) -// arrayPrototype.toJSON = arrayToJson; -// -// const promise = new Promise((resolve, reject) => { -// if (this._useXHR) { -// sendWithXhr(body, this.url, this._headers, resolve, reject); -// } else { -// sendWithBeacon( -// body, -// this.url, -// { type: 'application/json' }, -// resolve, -// reject -// ); -// } -// }).then(onSuccess, onError); -// -// this._sendingPromises.push(promise); -// const popPromise = () => { -// const index = this._sendingPromises.indexOf(promise); -// this._sendingPromises.splice(index, 1); -// }; -// promise.then(popPromise, popPromise); -// } -// -// // declares the global Prototype variable -// declare const Prototype: any; -// -// export function patchExporterClass() { -// // Patches the `send`function of the trace exporter in order to handle -// // the span serialization correctly, when using Prototype < 1.7 -// const arrayPrototype: any = Array.prototype; -// if ( -// typeof Prototype !== 'undefined' && -// parseFloat(Prototype.Version.substr(0, 3)) < 1.7 && -// typeof arrayPrototype.toJSON !== 'undefined' -// ) { -// CollectorTraceExporter.prototype.send = sendPatch; -// } -// } +/** + * The original OpenTelemetry Collector span exporter used the `Array.from` method which is overridden by Prototype. The Prototype's function does not provide + * all functionality of the original function, thus, the exporter will fail exporting spans in case Prototype is used. + * See: https://github.com/prototypejs/prototype/issues/338 + * <> + * The original exporter is using the `JSON.stringify` method. This method is calling `toJSON` functions on the object to serialize. + * Unfortunately, prototype is adding a `toJSON` method to the Array class in versions prior 1.7. This leads to the problem, that nested + * arrays are stringified separately, thus, they are considered not as an array anymore but as a string resulting in an invalid JSON string. + * See: https://stackoverflow.com/questions/29637962/json-stringify-turned-the-value-array-into-a-string/29638420#29638420 + */ +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { diag } from '@opentelemetry/api'; +import * as otlpTypes from '@opentelemetry/otlp-exporter-base/build/src/types'; +import { sendWithBeacon, sendWithXhr } from '@opentelemetry/otlp-exporter-base/build/src/platform/browser/util'; + +/** + * Patches the `send` function of the trace exporter in order to handle + * the span serialization correctly, when using Prototype < 1.7 + */ +export function patchExporterClass() { + const arrayPrototype: any = Array.prototype; + if ( + typeof Prototype !== 'undefined' && + parseFloat(Prototype.Version.substr(0, 3)) < 1.7 && + typeof arrayPrototype.toJSON !== 'undefined' + ) { + OTLPTraceExporter.prototype.send = sendPatch; + } +} + +/** + * This function is basically a copy of the `send` function of the following file: + * https://github.com/open-telemetry/opentelemetry-js/blob/experimental/v0.48.0/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts + * + * Here, a "fix" has been added in order to support Prototype prior 1.7. + */ +function sendPatch( + items: any[], + onSuccess: () => void, + onError: (error: otlpTypes.OTLPExporterError) => void +): void { + if (this._shutdownOnce.isCalled) { + diag.debug('Shutdown already started. Cannot send objects'); + return; + } + const serviceRequest = this.convert(items); + + // START fix + // in order to fix the problem, we temporarily remove the `toJSON` + // function (1), serializing the spans (2) and reading the function (3) + // in order to preserve the initial state of the class + + // (1) + const arrayPrototype: any = Array.prototype; + const arrayToJson = arrayPrototype.toJSON; + delete arrayPrototype.toJSON; + // (2) + const body = JSON.stringify(serviceRequest); + // (3) + arrayPrototype.toJSON = arrayToJson; + // END fix + + const promise = new Promise((resolve, reject) => { + if (this._useXHR) { + sendWithXhr( + body, + this.url, + this._headers, + this.timeoutMillis, + resolve, + reject + ); + } else { + sendWithBeacon( + body, + this.url, + { type: 'application/json' }, + resolve, + reject + ); + } + }).then(onSuccess, onError); + + this._sendingPromises.push(promise); + const popPromise = () => { + const index = this._sendingPromises.indexOf(promise); + this._sendingPromises.splice(index, 1); + }; + promise.then(popPromise, popPromise); +} + +// declares the global Prototype variable +declare const Prototype: any; diff --git a/src/types.d.ts b/src/types.d.ts index 7ba6cff..c6a5f8f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -17,6 +17,7 @@ export interface PluginProperties { commonAttributes: StringMap; serviceName: string | (() => string); propagationHeader: PropagationHeader; + prototypeExporterPatch: boolean; } export const enum PropagationHeader {