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;