diff --git a/examples/collector-exporter-node/README.md b/examples/collector-exporter-node/README.md index 9d61c6b1687..e1b19ffef95 100644 --- a/examples/collector-exporter-node/README.md +++ b/examples/collector-exporter-node/README.md @@ -20,16 +20,32 @@ npm install npm run docker:start ``` -2. Run app +2. Run tracing app ```shell script # from this directory - npm start + npm start:tracing ``` -3. Open page at - you should be able to see the spans in zipkin +3. Run metrics app + + ```shell script + # from this directory + npm start:metrics + ``` + +4. Open page at - you should be able to see the spans in zipkin ![Screenshot of the running example](images/spans.png) +### Prometheus UI + +The prometheus client will be available at . + +Note: It may take some time for the application metrics to appear on the Prometheus dashboard. + +

+

+ ## Useful links - For more information on OpenTelemetry, visit: diff --git a/examples/collector-exporter-node/docker/collector-config.yaml b/examples/collector-exporter-node/docker/collector-config.yaml index f104677f7eb..e9a909d78fa 100644 --- a/examples/collector-exporter-node/docker/collector-config.yaml +++ b/examples/collector-exporter-node/docker/collector-config.yaml @@ -10,6 +10,8 @@ receivers: exporters: zipkin: endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" + prometheus: + endpoint: "0.0.0.0:9464" processors: batch: @@ -21,3 +23,7 @@ service: receivers: [otlp] exporters: [zipkin] processors: [batch, queued_retry] + metrics: + receivers: [otlp] + exporters: [prometheus] + processors: [batch, queued_retry] diff --git a/examples/collector-exporter-node/docker/docker-compose.yaml b/examples/collector-exporter-node/docker/docker-compose.yaml index 3882379ad35..0dfe1a23f70 100644 --- a/examples/collector-exporter-node/docker/docker-compose.yaml +++ b/examples/collector-exporter-node/docker/docker-compose.yaml @@ -5,11 +5,10 @@ services: image: otel/opentelemetry-collector:latest # image: otel/opentelemetry-collector:0.6.0 command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] - networks: - - otelcol volumes: - ./collector-config.yaml:/conf/collector-config.yaml ports: + - "9464:9464" - "55680:55680" - "55681:55681" depends_on: @@ -18,10 +17,14 @@ services: # Zipkin zipkin-all-in-one: image: openzipkin/zipkin:latest - networks: - - otelcol ports: - "9411:9411" -networks: - otelcol: + # Prometheus + prometheus: + container_name: prometheus + image: prom/prometheus:latest + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" diff --git a/examples/collector-exporter-node/docker/prometheus.yaml b/examples/collector-exporter-node/docker/prometheus.yaml new file mode 100644 index 00000000000..b027daf9a0b --- /dev/null +++ b/examples/collector-exporter-node/docker/prometheus.yaml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s # Default is every 1 minute. + +scrape_configs: + - job_name: 'collector' + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + static_configs: + - targets: ['collector:9464'] diff --git a/examples/collector-exporter-node/metrics.js b/examples/collector-exporter-node/metrics.js new file mode 100644 index 00000000000..f91e105c201 --- /dev/null +++ b/examples/collector-exporter-node/metrics.js @@ -0,0 +1,29 @@ +'use strict'; + +const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector'); +const { MeterProvider } = require('@opentelemetry/metrics'); + +const metricExporter = new CollectorMetricExporter({ + serviceName: 'basic-metric-service', + // logger: new ConsoleLogger(LogLevel.DEBUG), +}); + +const meter = new MeterProvider({ + exporter: metricExporter, + interval: 1000, +}).getMeter('example-prometheus'); + +const requestCounter = meter.createCounter('requests', { + description: 'Example of a Counter', +}); + +const upDownCounter = meter.createUpDownCounter('test_up_down_counter', { + description: 'Example of a UpDownCounter', +}); + +const labels = { pid: process.pid, environment: 'staging' }; + +setInterval(() => { + requestCounter.bind(labels).add(1); + upDownCounter.bind(labels).add(Math.random() > 0.5 ? 1 : -1); +}, 1000); diff --git a/examples/collector-exporter-node/package.json b/examples/collector-exporter-node/package.json index 8e3b8210287..7dfeb695fb8 100644 --- a/examples/collector-exporter-node/package.json +++ b/examples/collector-exporter-node/package.json @@ -5,7 +5,8 @@ "description": "Example of using @opentelemetry/collector-exporter in Node.js", "main": "index.js", "scripts": { - "start": "node ./start.js", + "start:tracing": "node tracing.js", + "start:metrics": "node metrics.js", "docker:start": "cd ./docker && docker-compose down && docker-compose up", "docker:startd": "cd ./docker && docker-compose down && docker-compose up -d", "docker:stop": "cd ./docker && docker-compose down" @@ -30,6 +31,7 @@ "@opentelemetry/api": "^0.10.2", "@opentelemetry/core": "^0.10.2", "@opentelemetry/exporter-collector": "^0.10.2", + "@opentelemetry/metrics": "^0.10.2", "@opentelemetry/tracing": "^0.10.2" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme" diff --git a/examples/collector-exporter-node/start.js b/examples/collector-exporter-node/tracing.js similarity index 100% rename from examples/collector-exporter-node/start.js rename to examples/collector-exporter-node/tracing.js diff --git a/packages/opentelemetry-exporter-collector/README.md b/packages/opentelemetry-exporter-collector/README.md index 8233384dd15..2c04c069289 100644 --- a/packages/opentelemetry-exporter-collector/README.md +++ b/packages/opentelemetry-exporter-collector/README.md @@ -14,7 +14,7 @@ This module provides exporter for web and node to be used with [opentelemetry-co npm install --save @opentelemetry/exporter-collector ``` -## Usage in Web +## Traces in Web The CollectorTraceExporter in Web expects the endpoint to end in `/v1/trace`. @@ -36,7 +36,32 @@ provider.register(); ``` -## Usage in Node - GRPC +## Metrics in Web + +The CollectorMetricExporter in Web expects the endpoint to end in `/v1/metrics`. + +```js +import { MetricProvider } from '@opentelemetry/metrics'; +import { CollectorMetricExporter } from '@opentelemetry/exporter-collector'; +const collectorOptions = { + url: '', // url is optional and can be omitted - default is http://localhost:55681/v1/metrics + headers: {}, //an optional object containing custom headers to be sent with each request +}; +const exporter = new CollectorMetricExporter(collectorOptions); + +// Register the exporter +const meter = new MeterProvider({ + exporter, + interval: 60000, +}).getMeter('example-meter'); + +// Now, start recording data +const counter = meter.createCounter('metric_name'); +counter.add(10, { 'key': 'value' }); + +``` + +## Traces in Node - GRPC The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/trace`. @@ -109,7 +134,7 @@ provider.register(); Note, that this will only work if TLS is also configured on the server. -## Usage in Node - JSON over http +## Traces in Node - JSON over http ```js const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); @@ -132,7 +157,7 @@ provider.register(); ``` -## Usage in Node - PROTO over http +## Traces in Node - PROTO over http ```js const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); @@ -155,26 +180,28 @@ provider.register(); ``` -## Usage in Node - PROTO over http +## Metrics in Node -```js -const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorExporter, CollectorTransportNode } = require('@opentelemetry/exporter-collector'); +The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/metrics`. All options that work with trace also work with metrics. +```js +const { MeterProvider } = require('@opentelemetry/metrics'); +const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector'); const collectorOptions = { - protocolNode: CollectorTransportNode.HTTP_PROTO, serviceName: 'basic-service', - url: '', // url is optional and can be omitted - default is http://localhost:55680/v1/trace - headers: { - foo: 'bar' - }, //an optional object containing custom headers to be sent with each request will only work with json over http + url: '', // url is optional and can be omitted - default is localhost:55681 }; +const exporter = new CollectorMetricExporter(collectorOptions); -const provider = new BasicTracerProvider(); -const exporter = new CollectorExporter(collectorOptions); -provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); +// Register the exporter +const meter = new MeterProvider({ + exporter, + interval: 60000, +}).getMeter('example-meter'); -provider.register(); +// Now, start recording data +const counter = meter.createCounter('metric_name'); +counter.add(10, { 'key': 'value' }); ``` diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts new file mode 100644 index 00000000000..ab35cd98ac8 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CollectorExporterBase } from '../../CollectorExporterBase'; +import { CollectorExporterConfigBrowser } from './types'; +import * as collectorTypes from '../../types'; +import { parseHeaders } from '../../util'; +import { sendWithBeacon, sendWithXhr } from './util'; + +/** + * Collector Metric Exporter abstract base class + */ +export abstract class CollectorExporterBrowserBase< + ExportItem, + ServiceRequest +> extends CollectorExporterBase< + CollectorExporterConfigBrowser, + ExportItem, + ServiceRequest +> { + private _headers: Record; + private _useXHR: boolean = false; + + /** + * @param config + */ + constructor(config: CollectorExporterConfigBrowser = {}) { + super(config); + this._useXHR = + !!config.headers || typeof navigator.sendBeacon !== 'function'; + if (this._useXHR) { + this._headers = parseHeaders(config.headers, this.logger); + } else { + this._headers = {}; + } + } + + onInit(): void { + window.addEventListener('unload', this.shutdown); + } + + onShutdown(): void { + window.removeEventListener('unload', this.shutdown); + } + + send( + items: ExportItem[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void + ) { + const serviceRequest = this.convert(items); + const body = JSON.stringify(serviceRequest); + + if (this._useXHR) { + sendWithXhr( + body, + this.url, + this._headers, + this.logger, + onSuccess, + onError + ); + } else { + sendWithBeacon(body, this.url, this.logger, onSuccess, onError); + } + } +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts new file mode 100644 index 00000000000..772f7fca297 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorMetricExporter.ts @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MetricRecord, MetricExporter } from '@opentelemetry/metrics'; +import * as collectorTypes from '../../types'; +import { CollectorExporterBrowserBase } from './CollectorExporterBrowserBase'; +import { toCollectorExportMetricServiceRequest } from '../../transformMetrics'; +import { CollectorExporterConfigBrowser } from './types'; + +const DEFAULT_COLLECTOR_URL = 'http://localhost:55680/v1/metrics'; +const DEFAULT_SERVICE_NAME = 'collector-metric-exporter'; + +/** + * Collector Metric Exporter for Web + */ +export class CollectorMetricExporter + extends CollectorExporterBrowserBase< + MetricRecord, + collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest + > + implements MetricExporter { + // Converts time to nanoseconds + private readonly _startTime = new Date().getTime() * 1000000; + + convert( + metrics: MetricRecord[] + ): collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest { + return toCollectorExportMetricServiceRequest( + metrics, + this._startTime, + this + ); + } + + getDefaultUrl(config: CollectorExporterConfigBrowser): string { + return config.url || DEFAULT_COLLECTOR_URL; + } + + getDefaultServiceName(config: CollectorExporterConfigBrowser): string { + return config.serviceName || DEFAULT_SERVICE_NAME; + } +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts index d593e1d44db..6fb89b46cf8 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import { CollectorExporterBase } from '../../CollectorExporterBase'; +import { CollectorExporterBrowserBase } from './CollectorExporterBrowserBase'; import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; import { toCollectorExportTraceServiceRequest } from '../../transform'; import { CollectorExporterConfigBrowser } from './types'; import * as collectorTypes from '../../types'; -import { sendWithBeacon, sendWithXhr } from './util'; -import { parseHeaders } from '../../util'; const DEFAULT_SERVICE_NAME = 'collector-trace-exporter'; const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/trace'; @@ -29,35 +27,15 @@ const DEFAULT_COLLECTOR_URL = 'http://localhost:55681/v1/trace'; * Collector Trace Exporter for Web */ export class CollectorTraceExporter - extends CollectorExporterBase< - CollectorExporterConfigBrowser, + extends CollectorExporterBrowserBase< ReadableSpan, collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest > implements SpanExporter { - private _headers: Record; - private _useXHR: boolean = false; - - /** - * @param config - */ - constructor(config: CollectorExporterConfigBrowser = {}) { - super(config); - this._useXHR = - !!config.headers || typeof navigator.sendBeacon !== 'function'; - if (this._useXHR) { - this._headers = parseHeaders(config.headers, this.logger); - } else { - this._headers = {}; - } - } - - onInit(): void { - window.addEventListener('unload', this.shutdown); - } - - onShutdown(): void { - window.removeEventListener('unload', this.shutdown); + convert( + spans: ReadableSpan[] + ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + return toCollectorExportTraceServiceRequest(spans, this); } getDefaultUrl(config: CollectorExporterConfigBrowser) { @@ -67,32 +45,4 @@ export class CollectorTraceExporter getDefaultServiceName(config: CollectorExporterConfigBrowser): string { return config.serviceName || DEFAULT_SERVICE_NAME; } - - convert( - spans: ReadableSpan[] - ): collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { - return toCollectorExportTraceServiceRequest(spans, this); - } - - send( - spans: ReadableSpan[], - onSuccess: () => void, - onError: (error: collectorTypes.CollectorExporterError) => void - ) { - const exportTraceServiceRequest = this.convert(spans); - const body = JSON.stringify(exportTraceServiceRequest); - - if (this._useXHR) { - sendWithXhr( - body, - this.url, - this._headers, - this.logger, - onSuccess, - onError - ); - } else { - sendWithBeacon(body, this.url, this.logger, onSuccess, onError); - } - } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts index 1c17973de2e..fcbe012b52b 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts @@ -15,3 +15,4 @@ */ export * from './CollectorTraceExporter'; +export * from './CollectorMetricExporter'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts index 3f0aa3c8def..8eebc7bdebe 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorMetricExporter.ts @@ -35,6 +35,7 @@ export class CollectorMetricExporter collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest > implements MetricExporter { + // Converts time to nanoseconds protected readonly _startTime = new Date().getTime() * 1000000; convert( diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts new file mode 100644 index 00000000000..8187bebdf12 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorMetricExporter.test.ts @@ -0,0 +1,403 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NoopLogger } from '@opentelemetry/core'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { CollectorMetricExporter } from '../../src/platform/browser/index'; +import * as collectorTypes from '../../src/types'; +import { MetricRecord } from '@opentelemetry/metrics'; +import { + mockCounter, + mockObserver, + ensureCounterIsCorrect, + ensureObserverIsCorrect, + ensureWebResourceIsCorrect, + ensureExportMetricsServiceRequestIsSet, + ensureHeadersContain, + mockHistogram, + mockValueRecorder, + ensureValueRecorderIsCorrect, + ensureHistogramIsCorrect, +} from '../helper'; +import { CollectorExporterConfigBrowser } from '../../src/platform/browser/types'; +import { hrTimeToNanoseconds } from '@opentelemetry/core'; +const sendBeacon = navigator.sendBeacon; + +describe('CollectorMetricExporter - web', () => { + let collectorExporter: CollectorMetricExporter; + let spyOpen: any; + let spySend: any; + let spyBeacon: any; + let metrics: MetricRecord[]; + + beforeEach(() => { + spyOpen = sinon.stub(XMLHttpRequest.prototype, 'open'); + spySend = sinon.stub(XMLHttpRequest.prototype, 'send'); + spyBeacon = sinon.stub(navigator, 'sendBeacon'); + metrics = []; + metrics.push(mockCounter()); + metrics.push(mockObserver()); + metrics.push(mockHistogram()); + metrics.push(mockValueRecorder()); + + metrics[0].aggregator.update(1); + metrics[1].aggregator.update(3); + metrics[1].aggregator.update(6); + metrics[2].aggregator.update(7); + metrics[2].aggregator.update(14); + metrics[3].aggregator.update(5); + }); + + afterEach(() => { + navigator.sendBeacon = sendBeacon; + spyOpen.restore(); + spySend.restore(); + spyBeacon.restore(); + }); + + describe('export', () => { + describe('when "sendBeacon" is available', () => { + beforeEach(() => { + collectorExporter = new CollectorMetricExporter({ + logger: new NoopLogger(), + url: 'http://foo.bar.com', + serviceName: 'bar', + }); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, + }); + }); + it('should successfully send metrics using sendBeacon', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const args = spyBeacon.args[0]; + const url = args[0]; + const body = args[1]; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + const metric1 = + json.resourceMetrics[0].instrumentationLibraryMetrics[0].metrics[0]; + const metric2 = + json.resourceMetrics[1].instrumentationLibraryMetrics[0].metrics[0]; + const metric3 = + json.resourceMetrics[2].instrumentationLibraryMetrics[0].metrics[0]; + const metric4 = + json.resourceMetrics[3].instrumentationLibraryMetrics[0].metrics[0]; + assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); + if (metric1) { + ensureCounterIsCorrect( + metric1, + hrTimeToNanoseconds(metrics[0].aggregator.toPoint().timestamp) + ); + } + + assert.ok( + typeof metric2 !== 'undefined', + "second metric doesn't exist" + ); + if (metric2) { + ensureObserverIsCorrect( + metric2, + hrTimeToNanoseconds(metrics[1].aggregator.toPoint().timestamp) + ); + } + + assert.ok( + typeof metric3 !== 'undefined', + "third metric doesn't exist" + ); + if (metric3) { + ensureHistogramIsCorrect( + metric3, + hrTimeToNanoseconds(metrics[2].aggregator.toPoint().timestamp) + ); + } + + assert.ok( + typeof metric4 !== 'undefined', + "fourth metric doesn't exist" + ); + if (metric4) { + ensureValueRecorderIsCorrect( + metric4, + hrTimeToNanoseconds(metrics[3].aggregator.toPoint().timestamp) + ); + } + + const resource = json.resourceMetrics[0].resource; + assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(spyBeacon.callCount, 1); + + assert.strictEqual(spyOpen.callCount, 0); + + ensureExportMetricsServiceRequestIsSet(json); + + done(); + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + spyBeacon.restore(); + spyBeacon = sinon.stub(window.navigator, 'sendBeacon').returns(true); + + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'sendBeacon - can send'); + assert.strictEqual(spyLoggerError.args.length, 0); + + done(); + }); + }); + + it('should log the error message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + spyBeacon.restore(); + spyBeacon = sinon.stub(window.navigator, 'sendBeacon').returns(false); + + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'sendBeacon - cannot send'); + assert.strictEqual(spyLoggerDebug.args.length, 1); + + done(); + }); + }); + }); + + describe('when "sendBeacon" is NOT available', () => { + let server: any; + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + collectorExporter = new CollectorMetricExporter({ + logger: new NoopLogger(), + url: 'http://foo.bar.com', + serviceName: 'bar', + }); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, + }); + server = sinon.fakeServer.create(); + }); + afterEach(() => { + server.restore(); + }); + + it('should successfully send the metrics using XMLHttpRequest', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.opentelemetryProto.collector.metrics.v1.ExportMetricsServiceRequest; + const metric1 = + json.resourceMetrics[0].instrumentationLibraryMetrics[0].metrics[0]; + const metric2 = + json.resourceMetrics[1].instrumentationLibraryMetrics[0].metrics[0]; + const metric3 = + json.resourceMetrics[2].instrumentationLibraryMetrics[0].metrics[0]; + const metric4 = + json.resourceMetrics[3].instrumentationLibraryMetrics[0].metrics[0]; + assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); + if (metric1) { + ensureCounterIsCorrect( + metric1, + hrTimeToNanoseconds(metrics[0].aggregator.toPoint().timestamp) + ); + } + assert.ok( + typeof metric2 !== 'undefined', + "second metric doesn't exist" + ); + if (metric2) { + ensureObserverIsCorrect( + metric2, + hrTimeToNanoseconds(metrics[1].aggregator.toPoint().timestamp) + ); + } + + assert.ok( + typeof metric3 !== 'undefined', + "third metric doesn't exist" + ); + if (metric3) { + ensureHistogramIsCorrect( + metric3, + hrTimeToNanoseconds(metrics[2].aggregator.toPoint().timestamp) + ); + } + + assert.ok( + typeof metric4 !== 'undefined', + "fourth metric doesn't exist" + ); + if (metric4) { + ensureValueRecorderIsCorrect( + metric4, + hrTimeToNanoseconds(metrics[3].aggregator.toPoint().timestamp) + ); + } + + const resource = json.resourceMetrics[0].resource; + assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + + assert.strictEqual(spyBeacon.callCount, 0); + ensureExportMetricsServiceRequestIsSet(json); + + done(); + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(200); + + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'xhr success'); + assert.strictEqual(spyLoggerError.args.length, 0); + + assert.strictEqual(spyBeacon.callCount, 0); + done(); + }); + }); + + it('should log the error message', done => { + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(400); + + const response1: any = spyLoggerError.args[0][0]; + const response2: any = spyLoggerError.args[1][0]; + assert.strictEqual(response1, 'body'); + assert.strictEqual(response2, 'xhr error'); + + assert.strictEqual(spyBeacon.callCount, 0); + done(); + }); + }); + it('should send custom headers', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(200); + + assert.strictEqual(spyBeacon.callCount, 0); + done(); + }); + }); + }); + }); + + describe('export with custom headers', () => { + let server: any; + const customHeaders = { + foo: 'bar', + bar: 'baz', + }; + let collectorExporterConfig: CollectorExporterConfigBrowser; + + beforeEach(() => { + collectorExporterConfig = { + logger: new NoopLogger(), + headers: customHeaders, + }; + server = sinon.fakeServer.create(); + }); + + afterEach(() => { + server.restore(); + }); + + describe('when "sendBeacon" is available', () => { + beforeEach(() => { + collectorExporter = new CollectorMetricExporter( + collectorExporterConfig + ); + }); + it('should successfully send custom headers using XMLHTTPRequest', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const [{ requestHeaders }] = server.requests; + + ensureHeadersContain(requestHeaders, customHeaders); + assert.strictEqual(spyBeacon.callCount, 0); + assert.strictEqual(spyOpen.callCount, 0); + + done(); + }); + }); + }); + + describe('when "sendBeacon" is NOT available', () => { + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + collectorExporter = new CollectorMetricExporter( + collectorExporterConfig + ); + }); + + it('should successfully send metrics using XMLHttpRequest', done => { + collectorExporter.export(metrics, () => {}); + + setTimeout(() => { + const [{ requestHeaders }] = server.requests; + + ensureHeadersContain(requestHeaders, customHeaders); + assert.strictEqual(spyBeacon.callCount, 0); + assert.strictEqual(spyOpen.callCount, 0); + + done(); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/helper.ts b/packages/opentelemetry-exporter-collector/test/helper.ts index 697fa7ee90f..be51825e8f3 100644 --- a/packages/opentelemetry-exporter-collector/test/helper.ts +++ b/packages/opentelemetry-exporter-collector/test/helper.ts @@ -1243,7 +1243,7 @@ export function ensureExportMetricsServiceRequestIsSet( assert.strictEqual( resourceMetrics.length, 4, - 'resourceMetrics is the incorrect length' + 'resourceMetrics has incorrect length' ); const resource = resourceMetrics[0].resource;