diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 2fca9156dc9..3363d30f8d5 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -32,6 +32,7 @@ All notable changes to experimental packages in this project will be documented ### :house: (Internal) +* chore(otlp-exporter-\*-http): clean up tests [#5196](/~https://github.com/open-telemetry/opentelemetry-js/pull/5198) @pichlermarc * chore(otlp-exporter-\*-proto): clean up tests [#5196](/~https://github.com/open-telemetry/opentelemetry-js/pull/5199) @pichlermarc ## 0.55.0 diff --git a/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts index b02e56f0697..4870b6681c3 100644 --- a/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts @@ -17,16 +17,78 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { OTLPLogExporter } from '../../src/platform/browser'; +import { + LoggerProvider, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; -describe('OTLPLogExporter', () => { +/* + * NOTE: Tests here are not intended to test the underlying components directly. They are intended as a quick + * check if the correct components are used. Use the following packages to test details: + * - `@opentelemetry/oltp-exporter-base`: OTLP common exporter logic (handling of concurrent exports, ...), HTTP transport code + * - `@opentelemetry/otlp-transformer`: Everything regarding serialization and transforming internal representations to OTLP + */ + +describe('OTLPLogExporter', function () { afterEach(() => { sinon.restore(); }); - describe('constructor', () => { - it('should create an instance', () => { - const exporter = new OTLPLogExporter(); - assert.ok(exporter instanceof OTLPLogExporter); + describe('export', function () { + describe('when sendBeacon is available', function () { + it('should successfully send data using sendBeacon', async function () { + // arrange + const stubBeacon = sinon.stub(navigator, 'sendBeacon'); + const loggerProvider = new LoggerProvider(); + loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new OTLPLogExporter()) + ); + + // act + loggerProvider.getLogger('test-logger').emit({ body: 'test-body' }); + await loggerProvider.shutdown(); + + // assert + const args = stubBeacon.args[0]; + const blob: Blob = args[1] as unknown as Blob; + const body = await blob.text(); + assert.doesNotThrow( + () => JSON.parse(body), + 'expected requestBody to be in JSON format, but parsing failed' + ); + }); + }); + + describe('when sendBeacon is not available', function () { + beforeEach(function () { + // fake sendBeacon not being available + (window.navigator as any).sendBeacon = false; + }); + + it('should successfully send data using XMLHttpRequest', async function () { + // arrange + const server = sinon.fakeServer.create(); + const loggerProvider = new LoggerProvider(); + loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new OTLPLogExporter()) + ); + + // act + loggerProvider.getLogger('test-logger').emit({ body: 'test-body' }); + queueMicrotask(() => { + // simulate success response + server.requests[0].respond(200, {}, ''); + }); + await loggerProvider.shutdown(); + + // assert + const request = server.requests[0]; + const body = request.requestBody as unknown as Uint8Array; + assert.doesNotThrow( + () => JSON.parse(new TextDecoder().decode(body)), + 'expected requestBody to be in JSON format, but parsing failed' + ); + }); }); }); }); diff --git a/experimental/packages/exporter-logs-otlp-http/test/logHelper.ts b/experimental/packages/exporter-logs-otlp-http/test/logHelper.ts deleted file mode 100644 index 11facc4402e..00000000000 --- a/experimental/packages/exporter-logs-otlp-http/test/logHelper.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 { HrTime, TraceFlags } from '@opentelemetry/api'; -import { SeverityNumber } from '@opentelemetry/api-logs'; -import { Resource } from '@opentelemetry/resources'; -import * as assert from 'assert'; -import { VERSION } from '@opentelemetry/core'; -import { - IAnyValue, - IExportLogsServiceRequest, - IKeyValue, - ILogRecord, - IResource, -} from '@opentelemetry/otlp-transformer'; -import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; - -export const mockedReadableLogRecord: ReadableLogRecord = { - resource: Resource.default().merge( - new Resource({ - 'resource-attribute': 'some resource-attr value', - }) - ), - instrumentationScope: { - name: 'scope_name_1', - version: '0.1.0', - schemaUrl: 'http://url.to.schema', - }, - hrTime: [1680253513, 123241635] as HrTime, - hrTimeObserved: [1680253513, 123241635] as HrTime, - attributes: { - 'some-attribute': 'some attribute value', - }, - droppedAttributesCount: 0, - severityNumber: SeverityNumber.ERROR, - severityText: 'error', - body: 'some_log_body', - spanContext: { - traceFlags: TraceFlags.SAMPLED, - traceId: '1f1008dc8e270e85c40a0d7c3939b278', - spanId: '5e107261f64fa53e', - }, -}; -export function ensureExportedAttributesAreCorrect(attributes: IKeyValue[]) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'some-attribute', - value: { - stringValue: 'some attribute value', - }, - }, - ], - 'exported attributes are incorrect' - ); -} - -export function ensureExportedBodyIsCorrect(body?: IAnyValue) { - assert.deepStrictEqual( - body, - { stringValue: 'some_log_body' }, - 'exported attributes are incorrect' - ); -} - -export function ensureExportedLogRecordIsCorrect(logRecord: ILogRecord) { - ensureExportedBodyIsCorrect(logRecord.body); - ensureExportedAttributesAreCorrect(logRecord.attributes); - assert.deepStrictEqual( - logRecord.timeUnixNano, - '1680253513123241635', - 'timeUnixNano is wrong' - ); - assert.deepStrictEqual( - logRecord.observedTimeUnixNano, - '1680253513123241635', - 'observedTimeUnixNano is wrong' - ); - assert.strictEqual( - logRecord.severityNumber, - SeverityNumber.ERROR, - 'severityNumber is wrong' - ); - assert.strictEqual(logRecord.severityText, 'error', 'severityText is wrong'); - assert.strictEqual( - logRecord.droppedAttributesCount, - 0, - 'droppedAttributesCount is wrong' - ); - assert.strictEqual(logRecord.flags, TraceFlags.SAMPLED, 'flags is wrong'); -} - -export function ensureResourceIsCorrect(resource: IResource) { - assert.deepStrictEqual(resource, { - attributes: [ - { - key: 'service.name', - value: { - stringValue: `unknown_service:${process.argv0}`, - value: 'stringValue', - }, - }, - { - key: 'telemetry.sdk.language', - value: { - stringValue: 'nodejs', - value: 'stringValue', - }, - }, - { - key: 'telemetry.sdk.name', - value: { - stringValue: 'opentelemetry', - value: 'stringValue', - }, - }, - { - key: 'telemetry.sdk.version', - value: { - stringValue: VERSION, - value: 'stringValue', - }, - }, - { - key: 'resource-attribute', - value: { - stringValue: 'some resource-attr value', - value: 'stringValue', - }, - }, - ], - droppedAttributesCount: 0, - }); -} - -export function ensureExportLogsServiceRequestIsSet( - json: IExportLogsServiceRequest -) { - const resourceLogs = json.resourceLogs; - assert.strictEqual(resourceLogs?.length, 1, 'resourceLogs is missing'); - - const resource = resourceLogs?.[0].resource; - assert.ok(resource, 'resource is missing'); - - const scopeLogs = resourceLogs?.[0].scopeLogs; - assert.strictEqual(scopeLogs?.length, 1, 'scopeLogs is missing'); - - const scope = scopeLogs?.[0].scope; - assert.ok(scope, 'scope is missing'); - - const logRecords = scopeLogs?.[0].logRecords; - assert.strictEqual(logRecords?.length, 1, 'logs are missing'); -} diff --git a/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts index dd31888e14b..b746f83db2b 100644 --- a/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts @@ -14,136 +14,31 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; import * as assert from 'assert'; import * as http from 'http'; import * as sinon from 'sinon'; import { OTLPLogExporter } from '../../src/platform/node'; -import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; import { - ensureExportLogsServiceRequestIsSet, - ensureExportedLogRecordIsCorrect, - mockedReadableLogRecord, -} from '../logHelper'; -import { PassThrough, Stream } from 'stream'; -import { IExportLogsServiceRequest } from '@opentelemetry/otlp-transformer'; -import { ExportResultCode } from '@opentelemetry/core'; + LoggerProvider, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; +import { Stream } from 'stream'; -let fakeRequest: PassThrough; - -class MockedResponse extends Stream { - constructor( - private _code: number, - private _msg?: string - ) { - super(); - } - - send(data: Uint8Array) { - this.emit('data', data); - this.emit('end'); - } - - get statusCode() { - return this._code; - } - - get statusMessage() { - return this._msg; - } -} +/* + * NOTE: Tests here are not intended to test the underlying components directly. They are intended as a quick + * check if the correct components are used. Use the following packages to test details: + * - `@opentelemetry/oltp-exporter-base`: OTLP common exporter logic (handling of concurrent exports, ...), HTTP transport code + * - `@opentelemetry/otlp-transformer`: Everything regarding serialization and transforming internal representations to OTLP + */ describe('OTLPLogExporter', () => { - let collectorExporter: OTLPLogExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let logs: ReadableLogRecord[]; - - afterEach(() => { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - sinon.restore(); - }); - - describe('constructor', () => { - it('should create an instance', () => { - const exporter = new OTLPLogExporter(); - assert.ok(exporter instanceof OTLPLogExporter); - }); - }); - describe('export', () => { - beforeEach(() => { - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - httpAgentOptions: { keepAliveMsecs: 2000 }, - }; - collectorExporter = new OTLPLogExporter(collectorExporterConfig); - logs = []; - logs.push(Object.assign({}, mockedReadableLogRecord)); - }); afterEach(() => { sinon.restore(); }); - it('should open the connection', done => { - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - cb(mockRes); - mockRes.send(Buffer.from('success')); - done(); - }); - return fakeRequest as any; - }); - collectorExporter.export(logs, () => {}); - }); - - it('should set custom headers', done => { - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - assert.strictEqual(options.headers['foo'], 'bar'); - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - cb(mockRes); - mockRes.send(Buffer.from('success')); - done(); - }); - return fakeRequest as any; - }); - - collectorExporter.export(logs, () => {}); - }); - - it('should have keep alive and keepAliveMsecs option set', done => { - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - assert.strictEqual(options.agent.keepAlive, true); - assert.strictEqual(options.agent.options.keepAliveMsecs, 2000); - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - cb(mockRes); - mockRes.send(Buffer.from('success')); - done(); - }); - return fakeRequest as any; - }); - - collectorExporter.export(logs, () => {}); - }); - - it('should successfully send the logs', done => { + it('successfully exports data', done => { const fakeRequest = new Stream.PassThrough(); Object.defineProperty(fakeRequest, 'setTimeout', { value: function (_timeout: number) {}, @@ -152,64 +47,28 @@ describe('OTLPLogExporter', () => { sinon.stub(http, 'request').returns(fakeRequest as any); let buff = Buffer.from(''); fakeRequest.on('finish', () => { - const responseBody = buff.toString(); - const json = JSON.parse(responseBody) as IExportLogsServiceRequest; - const log1 = json.resourceLogs?.[0].scopeLogs?.[0].logRecords?.[0]; - assert.ok(typeof log1 !== 'undefined', "log doesn't exist"); - ensureExportedLogRecordIsCorrect(log1); - - ensureExportLogsServiceRequestIsSet(json); - - done(); + try { + const requestBody = buff.toString(); + assert.doesNotThrow(() => { + JSON.parse(requestBody); + }, 'expected requestBody to be in JSON format, but parsing failed'); + done(); + } catch (e) { + done(e); + } }); fakeRequest.on('data', chunk => { buff = Buffer.concat([buff, chunk]); }); - const clock = sinon.useFakeTimers(); - collectorExporter.export(logs, () => {}); - clock.tick(200); - clock.restore(); - }); - - it('should log the successful message', done => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - const spyLoggerError = sinon.stub(diag, 'error'); - - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - cb(mockRes); - mockRes.send(Buffer.from('success')); - }); - return fakeRequest as any; - }); + const loggerProvider = new LoggerProvider(); + loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new OTLPLogExporter()) + ); - collectorExporter.export(logs, result => { - assert.strictEqual(result.code, ExportResultCode.SUCCESS); - assert.strictEqual(spyLoggerError.args.length, 0); - done(); - }); - }); - - it('should log the error message', done => { - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - queueMicrotask(() => { - const mockRes = new MockedResponse(400); - cb(mockRes); - mockRes.send(Buffer.from('failure')); - }); - - return fakeRequest as any; - }); - - collectorExporter.export(logs, result => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - // @ts-expect-error verify error code - assert.strictEqual(result.error.code, 400); - done(); - }); + loggerProvider.getLogger('test-logger').emit({ body: 'test-body' }); + loggerProvider.shutdown(); }); }); }); diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts deleted file mode 100644 index 1de1e9485b0..00000000000 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ /dev/null @@ -1,560 +0,0 @@ -/* - * 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 * as core from '@opentelemetry/core'; -import { diag, DiagLogger, DiagLogLevel } from '@opentelemetry/api'; -import { ExportResultCode } from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { OTLPTraceExporter } from '../../src/platform/browser/index'; -import { - ensureSpanIsCorrect, - ensureExportTraceServiceRequestIsSet, - ensureWebResourceIsCorrect, - ensureHeadersContain, - mockedReadableSpan, -} from '../traceHelper'; -import { - OTLPExporterConfigBase, - OTLPExporterError, -} from '@opentelemetry/otlp-exporter-base'; -import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; - -describe('OTLPTraceExporter - web', () => { - let collectorTraceExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterConfigBase; - let stubOpen: sinon.SinonStub; - let stubBeacon: sinon.SinonStub; - let spans: ReadableSpan[]; - - beforeEach(() => { - stubOpen = sinon.stub(XMLHttpRequest.prototype, 'open'); - sinon.stub(XMLHttpRequest.prototype, 'send'); - stubBeacon = sinon.stub(navigator, 'sendBeacon'); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe('constructor', () => { - beforeEach(() => { - collectorExporterConfig = { - url: 'http://foo.bar.com', - }; - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - - it('should create an instance', () => { - assert.ok(typeof collectorTraceExporter !== 'undefined'); - }); - }); - - describe('export', () => { - beforeEach(() => { - collectorExporterConfig = { - url: 'http://foo.bar.com', - }; - }); - - describe('when "sendBeacon" is available', () => { - beforeEach(() => { - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - - it('should successfully send the spans using sendBeacon', done => { - collectorTraceExporter.export(spans, () => {}); - - setTimeout(async () => { - try { - const args = stubBeacon.args[0]; - const url = args[0]; - const blob: Blob = args[1]; - const body = await blob.text(); - const json = JSON.parse(body) as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureSpanIsCorrect(span1); - - const resource = json.resourceSpans?.[0].resource; - assert.ok( - typeof resource !== 'undefined', - "resource doesn't exist" - ); - ensureWebResourceIsCorrect(resource); - - assert.strictEqual(url, 'http://foo.bar.com'); - assert.strictEqual(stubBeacon.callCount, 1); - - assert.strictEqual(stubOpen.callCount, 0); - - ensureExportTraceServiceRequestIsSet(json); - done(); - } catch (err) { - done(err); - } - }); - }); - - it('should log the successful message', done => { - const spyLoggerDebug = sinon.stub(); - const spyLoggerError = sinon.stub(); - const nop = () => {}; - const diagLogger: DiagLogger = { - debug: spyLoggerDebug, - error: spyLoggerError, - info: nop, - verbose: nop, - warn: nop, - }; - - diag.setLogger(diagLogger, DiagLogLevel.ALL); - - stubBeacon.returns(true); - - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(() => { - try { - sinon.assert.calledWith(spyLoggerDebug, 'SendBeacon success'); - sinon.assert.notCalled(spyLoggerError); - - done(); - } catch (e) { - done(e); - } - }); - }); - - it('should log the error message', done => { - stubBeacon.returns(false); - - collectorTraceExporter.export(spans, result => { - try { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok( - result.error, - 'Expected Error, but no Error was present on the result' - ); - assert.match(result.error?.message, /SendBeacon failed/); - done(); - } catch (e) { - done(e); - } - }); - }); - }); - - describe('when "sendBeacon" is NOT available', () => { - let server: any; - let clock: sinon.SinonFakeTimers; - beforeEach(() => { - // fakeTimers is used to replace the next setTimeout which is - // located in sendWithXhr function called by the export method - clock = sinon.useFakeTimers(); - - (window.navigator as any).sendBeacon = false; - server = sinon.fakeServer.create(); - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - afterEach(() => { - server.restore(); - }); - - it('should successfully send the spans using XMLHttpRequest', done => { - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(async () => { - try { - const request = server.requests[0]; - assert.strictEqual(request.method, 'POST'); - assert.strictEqual(request.url, 'http://foo.bar.com'); - - const body = request.requestBody as Uint8Array; - const json = JSON.parse( - new TextDecoder().decode(body) - ) as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureSpanIsCorrect(span1); - - const resource = json.resourceSpans?.[0].resource; - assert.ok( - typeof resource !== 'undefined', - "resource doesn't exist" - ); - ensureWebResourceIsCorrect(resource); - - assert.strictEqual(stubBeacon.callCount, 0); - ensureExportTraceServiceRequestIsSet(json); - - clock.restore(); - done(); - } catch (e) { - done(e); - } - }); - }); - - it('should log the successful message', done => { - const spyLoggerDebug = sinon.stub(); - const spyLoggerError = sinon.stub(); - const nop = () => {}; - const diagLogger: DiagLogger = { - debug: spyLoggerDebug, - error: spyLoggerError, - info: nop, - verbose: nop, - warn: nop, - }; - - diag.setLogger(diagLogger, DiagLogLevel.ALL); - - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(200); - try { - const response: any = spyLoggerDebug.args[2][0]; - assert.strictEqual(response, 'XHR success'); - assert.strictEqual(spyLoggerError.args.length, 0); - assert.strictEqual(stubBeacon.callCount, 0); - clock.restore(); - done(); - } catch (e) { - done(e); - } - }); - }); - - it('should log the error message', done => { - collectorTraceExporter.export(spans, result => { - try { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.deepStrictEqual( - result.error?.message, - 'XHR request failed with non-retryable status' - ); - } catch (e) { - done(e); - } - done(); - }); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(400); - }); - }); - - it('should send custom headers', done => { - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(200); - - assert.strictEqual(stubBeacon.callCount, 0); - clock.restore(); - done(); - }); - }); - }); - }); - - describe('export with custom headers', () => { - let server: any; - const customHeaders = { - foo: 'bar', - bar: 'baz', - }; - - beforeEach(() => { - collectorExporterConfig = { - headers: customHeaders, - }; - server = sinon.fakeServer.create(); - }); - - afterEach(() => { - server.restore(); - }); - - describe('when "sendBeacon" is available', () => { - let clock: sinon.SinonFakeTimers; - beforeEach(() => { - // fakeTimers is used to replace the next setTimeout which is - // located in sendWithXhr function called by the export method - clock = sinon.useFakeTimers(); - - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - it('should successfully send custom headers using XMLHTTPRequest', done => { - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(() => { - const [{ requestHeaders }] = server.requests; - - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); - - clock.restore(); - done(); - }); - }); - }); - - describe('when "sendBeacon" is NOT available', () => { - let clock: sinon.SinonFakeTimers; - beforeEach(() => { - // fakeTimers is used to replace the next setTimeout which is - // located in sendWithXhr function called by the export method - clock = sinon.useFakeTimers(); - - (window.navigator as any).sendBeacon = false; - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - - it('should successfully send spans using XMLHttpRequest', done => { - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(() => { - try { - const [{ requestHeaders: requestHeaders }] = server.requests; - - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); - done(); - } catch (e) { - done(e); - } finally { - clock.restore(); - } - }); - }); - it('should log the timeout request error message', done => { - const responseSpy = sinon.spy(); - collectorTraceExporter.export(spans, responseSpy); - clock.tick(20000); - clock.restore(); - - setTimeout(() => { - try { - const result = responseSpy.args[0][0] as core.ExportResult; - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'XHR request timed out'); - done(); - } catch (e) { - done(e); - } - }); - }); - }); - }); - - describe('export - concurrency limit', () => { - it('should error if too many concurrent exports are queued', done => { - const collectorExporterWithConcurrencyLimit = new OTLPTraceExporter({ - ...collectorExporterConfig, - concurrencyLimit: 3, - }); - const spans: ReadableSpan[] = [{ ...mockedReadableSpan }]; - const callbackSpy = sinon.spy(); - for (let i = 0; i < 7; i++) { - collectorExporterWithConcurrencyLimit.export(spans, callbackSpy); - } - - const failures = callbackSpy.args.filter( - ([result]) => result.code === ExportResultCode.FAILED - ); - - setTimeout(() => { - // Expect 4 failures - try { - assert.strictEqual(failures.length, 4); - failures.forEach(([result]) => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - assert.strictEqual( - result.error!.message, - 'Concurrent export limit reached' - ); - }); - done(); - } catch (e) { - done(e); - } - }); - }); - }); -}); - -describe('export with retry - real http request destroyed', () => { - let server: any; - let collectorTraceExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterConfigBase; - let spans: ReadableSpan[]; - - beforeEach(() => { - server = sinon.fakeServer.create({ - autoRespond: true, - }); - collectorExporterConfig = { - timeoutMillis: 1500, - }; - }); - - afterEach(() => { - server.restore(); - }); - - describe('when "sendBeacon" is NOT available', () => { - beforeEach(() => { - (window.navigator as any).sendBeacon = false; - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - it('should log the retryable request error message when retrying with exponential backoff with jitter', done => { - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - let calls = 0; - server.respondWith( - 'http://localhost:4318/v1/traces', - function (xhr: any) { - calls++; - xhr.respond(503); - } - ); - - collectorTraceExporter.export(spans, result => { - try { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual( - error.message, - 'Export failed with retryable status' - ); - assert.strictEqual(calls, 2); - done(); - } catch (e) { - done(e); - } - }); - }).timeout(3000); - - it('should log the timeout request error message when retry-after header is set to 3 seconds', done => { - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - let calls = 0; - server.respondWith( - 'http://localhost:4318/v1/traces', - function (xhr: any) { - calls++; - xhr.respond(503, { 'Retry-After': 0.1 }); - } - ); - - collectorTraceExporter.export(spans, result => { - try { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual( - error.message, - 'Export failed with retryable status' - ); - assert.strictEqual(calls, 6); - done(); - } catch (e) { - done(e); - } - }); - }).timeout(3000); - it('should log the timeout request error message when retry-after header is a date', done => { - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - let retry = 0; - server.respondWith( - 'http://localhost:4318/v1/traces', - function (xhr: any) { - retry++; - const d = new Date(); - d.setSeconds(d.getSeconds() + 0.1); - xhr.respond(503, { 'Retry-After': d }); - } - ); - - collectorTraceExporter.export(spans, result => { - try { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual( - error.message, - 'Export failed with retryable status' - ); - assert.strictEqual(retry, 6); - done(); - } catch (e) { - done(e); - } - }); - }).timeout(3000); - it('should log the timeout request error message when retry-after header is a date with long delay', done => { - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - let retry = 0; - server.respondWith( - 'http://localhost:4318/v1/traces', - function (xhr: any) { - retry++; - const d = new Date(); - d.setSeconds(d.getSeconds() + 120); - xhr.respond(503, { 'Retry-After': d }); - } - ); - - collectorTraceExporter.export(spans, result => { - try { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual( - error.message, - 'Export failed with retryable status' - ); - assert.strictEqual(retry, 1); - done(); - } catch (e) { - done(e); - } - }); - }).timeout(3000); - }); -}); diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/OTLPTraceExporter.test.ts new file mode 100644 index 00000000000..a5ef3ad0023 --- /dev/null +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/OTLPTraceExporter.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { + BasicTracerProvider, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { OTLPTraceExporter } from '../../src/platform/browser/index'; + +/* + * NOTE: Tests here are not intended to test the underlying components directly. They are intended as a quick + * check if the correct components are used. Use the following packages to test details: + * - `@opentelemetry/oltp-exporter-base`: OTLP common exporter logic (handling of concurrent exports, ...) + * - `@opentelemetry/otlp-transformer`: Everything regarding serialization and transforming internal representations to OTLP + * - `@opentelemetry/otlp-grpc-exporter-base`: gRPC transport + */ + +describe('OTLPTraceExporter', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('export', function () { + describe('when sendBeacon is available', function () { + it('should successfully send data using sendBeacon', async function () { + // arrange + const stubBeacon = sinon.stub(navigator, 'sendBeacon'); + const tracerProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())], + }); + + // act + tracerProvider.getTracer('test-tracer').startSpan('test-span').end(); + await tracerProvider.shutdown(); + + // assert + const args = stubBeacon.args[0]; + const blob: Blob = args[1] as unknown as Blob; + const body = await blob.text(); + assert.doesNotThrow( + () => JSON.parse(body), + 'expected requestBody to be in JSON format, but parsing failed' + ); + }); + }); + + describe('when sendBeacon is not available', function () { + beforeEach(function () { + // fake sendBeacon not being available + (window.navigator as any).sendBeacon = false; + }); + + it('should successfully send data using XMLHttpRequest', async function () { + // arrange + const server = sinon.fakeServer.create(); + const tracerProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())], + }); + + // act + tracerProvider.getTracer('test-tracer').startSpan('test-span').end(); + queueMicrotask(() => { + // simulate success response + server.requests[0].respond(200, {}, ''); + }); + await tracerProvider.shutdown(); + + // assert + const request = server.requests[0]; + const body = request.requestBody as unknown as Uint8Array; + assert.doesNotThrow( + () => JSON.parse(new TextDecoder().decode(body)), + 'expected requestBody to be in JSON format, but parsing failed' + ); + }); + }); + }); +}); diff --git a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts deleted file mode 100644 index 38e1bf5c2b2..00000000000 --- a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts +++ /dev/null @@ -1,442 +0,0 @@ -/* - * 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 * as assert from 'assert'; -import * as http from 'http'; -import * as sinon from 'sinon'; -import * as zlib from 'zlib'; -import { PassThrough, Stream } from 'stream'; - -import { diag } from '@opentelemetry/api'; -import { - CompressionAlgorithm, - OTLPExporterError, - OTLPExporterNodeConfigBase, -} from '@opentelemetry/otlp-exporter-base'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import * as core from '@opentelemetry/core'; -import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; - -import { MockedResponse } from './nodeHelpers'; -import { OTLPTraceExporter } from '../../src/platform/node'; -import { - ensureExportTraceServiceRequestIsSet, - ensureSpanIsCorrect, - mockedReadableSpan, -} from '../traceHelper'; - -let fakeRequest: PassThrough; - -const address = 'localhost:1501'; - -describe('OTLPTraceExporter - node with json over http', function () { - let collectorExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let stubRequest: sinon.SinonStub; - let spySetHeader: sinon.SinonSpy; - let spans: ReadableSpan[]; - - afterEach(function () { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - sinon.restore(); - }); - - describe('instance', function () { - it('should warn about metadata when using json', () => { - const metadata = 'foo'; - const warnLoggerSpy = sinon.stub(diag, 'warn'); - - collectorExporter = new OTLPTraceExporter({ - metadata, - url: address, - } as any); - sinon.assert.calledOnce(warnLoggerSpy); - sinon.assert.calledOnceWithExactly( - warnLoggerSpy, - 'Metadata cannot be set when using http' - ); - }); - }); - - describe('export', () => { - beforeEach(() => { - stubRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - httpAgentOptions: { keepAliveMsecs: 2000 }, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - - it('should open the connection', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - const options = args[0]; - - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); - done(); - }); - }); - - it('should set custom headers', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - const options = args[0]; - assert.strictEqual(options.headers['foo'], 'bar'); - done(); - }); - }); - - it('should not have Content-Encoding header', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - const options = args[0]; - assert.strictEqual(options.headers['Content-Encoding'], undefined); - done(); - }); - }); - - it('should have keep alive and keepAliveMsecs option set', done => { - collectorExporter.export(spans, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - const options = args[0]; - const agent = options.agent; - assert.strictEqual(agent.keepAlive, true); - assert.strictEqual(agent.options.keepAliveMsecs, 2000); - done(); - }); - }); - - it('different http export requests should use the same agent', done => { - const clock = sinon.useFakeTimers(); - collectorExporter.export(spans, () => {}); - - const args = stubRequest.args[0]; - const callback = args[1]; - const mockRes = new MockedResponse(200); - - queueMicrotask(() => { - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - clock.restore(); - - queueMicrotask(() => { - const clock = sinon.useFakeTimers(); - collectorExporter.export(spans, () => {}); - - const mockRes2 = new MockedResponse(200); - const args2 = stubRequest.args[1]; - const callback2 = args2[1]; - - callback2(mockRes); - mockRes2.send(Buffer.from('success')); - - const [firstExportAgent, secondExportAgent] = stubRequest.args.map( - a => a[0].agent - ); - - assert.strictEqual(firstExportAgent, secondExportAgent); - clock.restore(); - done(); - }); - }); - - it('should successfully send the spans', done => { - let buff = Buffer.from(''); - - fakeRequest.on('finish', () => { - const responseBody = buff.toString(); - const json = JSON.parse(responseBody) as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureSpanIsCorrect(span1); - - ensureExportTraceServiceRequestIsSet(json); - - done(); - }); - - fakeRequest.on('data', chunk => { - buff = Buffer.concat([buff, chunk]); - }); - - collectorExporter.export(spans, () => {}); - - const mockRes = new MockedResponse(200); - const args = stubRequest.args[0]; - const callback = args[1]; - - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - it('should log the successful message', done => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - const stubLoggerError = sinon.stub(diag, 'error'); - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - setTimeout(() => { - assert.strictEqual(stubLoggerError.args.length, 0); - assert.strictEqual( - responseSpy.args[0][0].code, - core.ExportResultCode.SUCCESS - ); - done(); - }); - }); - }); - - it('should log the error message', done => { - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - - queueMicrotask(() => { - const mockRes = new MockedResponse(400); - callback(mockRes); - mockRes.send(Buffer.from('failure')); - }); - - setTimeout(() => { - const result = responseSpy.args[0][0] as core.ExportResult; - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.code, 400); - done(); - }); - }); - }); - }); - - describe('export - with compression', () => { - beforeEach(() => { - stubRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - spySetHeader = sinon.spy(); - (fakeRequest as any).setHeader = spySetHeader; - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - compression: CompressionAlgorithm.GZIP, - httpAgentOptions: { keepAliveMsecs: 2000 }, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - - it('should successfully send the spans', done => { - let buff = Buffer.from(''); - - fakeRequest.on('finish', () => { - const responseBody = zlib.gunzipSync(buff).toString(); - - const json = JSON.parse(responseBody) as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureSpanIsCorrect(span1); - - ensureExportTraceServiceRequestIsSet(json); - assert.ok(spySetHeader.calledWith('Content-Encoding', 'gzip')); - done(); - }); - - fakeRequest.on('data', chunk => { - buff = Buffer.concat([buff, chunk]); - }); - - collectorExporter.export(spans, () => {}); - - const mockRes = new MockedResponse(200); - const args = stubRequest.args[0]; - const callback = args[1]; - - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - }); - - describe('export - with timeout', () => { - beforeEach(() => { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - stubRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - spySetHeader = sinon.spy(); - (fakeRequest as any).setHeader = spySetHeader; - (fakeRequest as any).abort = sinon.spy(); - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - httpAgentOptions: { keepAliveMsecs: 2000 }, - timeoutMillis: 100, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - - it('should log the timeout request error message', done => { - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - fakeRequest.emit('error', { code: 'ECONNRESET' }); - - setTimeout(() => { - const result = responseSpy.args[0][0] as core.ExportResult; - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.deepEqual(error, { code: 'ECONNRESET' }); - - done(); - }); - }, 300); - }); - }); -}); - -describe('export - real http request destroyed before response received', () => { - let collectorExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let spans: ReadableSpan[]; - - const server = http.createServer((_, res) => { - setTimeout(() => { - res.statusCode = 200; - res.end(); - }, 200); - }); - before(done => { - server.listen(8081, done); - }); - after(done => { - server.close(done); - }); - it('should log the timeout request error message when timeout is 1', done => { - collectorExporterConfig = { - url: 'http://localhost:8081', - timeoutMillis: 1, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - setTimeout(() => { - collectorExporter.export(spans, result => { - try { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - } catch (e) { - done(e); - } - done(); - }); - }, 0); - }); - it('should log the timeout request error message when timeout is 100', done => { - collectorExporterConfig = { - url: 'http://localhost:8081', - timeoutMillis: 100, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - setTimeout(() => { - collectorExporter.export(spans, result => { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); - }); - }, 0); - }); -}); diff --git a/experimental/packages/exporter-trace-otlp-http/test/node/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/node/OTLPTraceExporter.test.ts new file mode 100644 index 00000000000..d4ee19ca85a --- /dev/null +++ b/experimental/packages/exporter-trace-otlp-http/test/node/OTLPTraceExporter.test.ts @@ -0,0 +1,73 @@ +/* + * 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 * as assert from 'assert'; +import * as http from 'http'; +import * as sinon from 'sinon'; +import { Stream } from 'stream'; + +import { + BasicTracerProvider, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { OTLPTraceExporter } from '../../src/platform/node'; + +/* + * NOTE: Tests here are not intended to test the underlying components directly. They are intended as a quick + * check if the correct components are used. Use the following packages to test details: + * - `@opentelemetry/oltp-exporter-base`: OTLP common exporter logic (handling of concurrent exports, ...), HTTP transport code + * - `@opentelemetry/otlp-transformer`: Everything regarding serialization and transforming internal representations to OTLP + */ + +describe('OTLPTraceExporter', () => { + describe('export', () => { + afterEach(() => { + sinon.restore(); + }); + + it('successfully exports data', done => { + const fakeRequest = new Stream.PassThrough(); + Object.defineProperty(fakeRequest, 'setTimeout', { + value: function (_timeout: number) {}, + }); + + sinon.stub(http, 'request').returns(fakeRequest as any); + let buff = Buffer.from(''); + fakeRequest.on('finish', () => { + try { + const requestBody = buff.toString(); + assert.doesNotThrow(() => { + JSON.parse(requestBody); + }, 'expected requestBody to be in JSON format, but parsing failed'); + done(); + } catch (e) { + done(e); + } + }); + + fakeRequest.on('data', chunk => { + buff = Buffer.concat([buff, chunk]); + }); + + new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())], + }) + .getTracer('test-tracer') + .startSpan('test-span') + .end(); + }); + }); +}); diff --git a/experimental/packages/exporter-trace-otlp-http/test/node/nodeHelpers.ts b/experimental/packages/exporter-trace-otlp-http/test/node/nodeHelpers.ts deleted file mode 100644 index e63d21b17c9..00000000000 --- a/experimental/packages/exporter-trace-otlp-http/test/node/nodeHelpers.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { Stream } from 'stream'; - -export class MockedResponse extends Stream { - constructor( - private _code: number, - private _msg?: string - ) { - super(); - } - - send(data: Uint8Array) { - this.emit('data', data); - this.emit('end'); - } - - get statusCode() { - return this._code; - } - - get statusMessage() { - return this._msg; - } -} diff --git a/experimental/packages/exporter-trace-otlp-http/test/traceHelper.ts b/experimental/packages/exporter-trace-otlp-http/test/traceHelper.ts deleted file mode 100644 index e89062c9240..00000000000 --- a/experimental/packages/exporter-trace-otlp-http/test/traceHelper.ts +++ /dev/null @@ -1,443 +0,0 @@ -/* - * 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 { SpanStatusCode, TraceFlags } from '@opentelemetry/api'; -import { - hexToBase64, - InstrumentationLibrary, - VERSION, -} from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import * as assert from 'assert'; -import { - ESpanKind, - IEvent, - IExportTraceServiceRequest, - IKeyValue, - ILink, - IResource, - ISpan, -} from '@opentelemetry/otlp-transformer'; - -if (typeof Buffer === 'undefined') { - (window as any).Buffer = { - from: function (arr: []) { - return new Uint8Array(arr); - }, - }; -} - -const traceIdHex = '1f1008dc8e270e85c40a0d7c3939b278'; -const spanIdHex = '5e107261f64fa53e'; -const parentIdHex = '78a8915098864388'; - -export const mockedReadableSpan: ReadableSpan = { - name: 'documentFetch', - kind: 0, - spanContext: () => { - return { - traceId: '1f1008dc8e270e85c40a0d7c3939b278', - spanId: '5e107261f64fa53e', - traceFlags: TraceFlags.SAMPLED, - }; - }, - parentSpanId: '78a8915098864388', - startTime: [1574120165, 429803070], - endTime: [1574120165, 438688070], - ended: true, - status: { code: SpanStatusCode.OK }, - attributes: { component: 'document-load' }, - links: [ - { - context: { - traceId: '1f1008dc8e270e85c40a0d7c3939b278', - spanId: '78a8915098864388', - traceFlags: TraceFlags.SAMPLED, - }, - attributes: { component: 'document-load' }, - }, - ], - events: [ - { - name: 'fetchStart', - time: [1574120165, 429803070], - }, - { - name: 'domainLookupStart', - time: [1574120165, 429803070], - }, - { - name: 'domainLookupEnd', - time: [1574120165, 429803070], - }, - { - name: 'connectStart', - time: [1574120165, 429803070], - }, - { - name: 'connectEnd', - time: [1574120165, 429803070], - }, - { - name: 'requestStart', - time: [1574120165, 435513070], - }, - { - name: 'responseStart', - time: [1574120165, 436923070], - }, - { - name: 'responseEnd', - time: [1574120165, 438688070], - }, - ], - duration: [0, 8885000], - resource: Resource.default().merge( - new Resource({ - service: 'ui', - version: 1, - cost: 112.12, - }) - ), - instrumentationLibrary: { name: 'default', version: '0.0.1' }, - droppedAttributesCount: 0, - droppedEventsCount: 0, - droppedLinksCount: 0, -}; - -export const mockedResources: Resource[] = [ - new Resource({ name: 'resource 1' }), - new Resource({ name: 'resource 2' }), -]; - -export const mockedInstrumentationLibraries: InstrumentationLibrary[] = [ - { - name: 'lib1', - version: '0.0.1', - }, - { - name: 'lib2', - version: '0.0.2', - }, -]; - -export const basicTrace: ReadableSpan[] = [ - { - name: 'span1', - kind: 0, - spanContext: () => { - return { - traceId: '1f1008dc8e270e85c40a0d7c3939b278', - spanId: '5e107261f64fa53e', - traceFlags: TraceFlags.SAMPLED, - }; - }, - parentSpanId: '78a8915098864388', - startTime: [1574120165, 429803070], - endTime: [1574120165, 438688070], - ended: true, - status: { code: SpanStatusCode.OK }, - attributes: {}, - links: [], - events: [], - duration: [0, 8885000], - resource: mockedResources[0], - instrumentationLibrary: mockedInstrumentationLibraries[0], - droppedAttributesCount: 0, - droppedEventsCount: 0, - droppedLinksCount: 0, - }, - { - name: 'span2', - kind: 0, - spanContext: () => { - return { - traceId: '1f1008dc8e270e85c40a0d7c3939b278', - spanId: 'f64fa53e5e107261', - traceFlags: TraceFlags.SAMPLED, - }; - }, - parentSpanId: '78a8915098864388', - startTime: [1575120165, 439803070], - endTime: [1575120165, 448688070], - ended: true, - status: { code: SpanStatusCode.OK }, - attributes: {}, - links: [], - events: [], - duration: [0, 8775000], - resource: mockedResources[0], - instrumentationLibrary: mockedInstrumentationLibraries[0], - droppedAttributesCount: 0, - droppedEventsCount: 0, - droppedLinksCount: 0, - }, - { - name: 'span3', - kind: 0, - spanContext: () => { - return { - traceId: '1f1008dc8e270e85c40a0d7c3939b278', - spanId: '07261f64fa53e5e1', - traceFlags: TraceFlags.SAMPLED, - }; - }, - parentSpanId: 'a891578098864388', - startTime: [1575120165, 439803070], - endTime: [1575120165, 448688070], - ended: true, - status: { code: SpanStatusCode.OK }, - attributes: {}, - links: [], - events: [], - duration: [0, 8775000], - resource: mockedResources[0], - instrumentationLibrary: mockedInstrumentationLibraries[0], - droppedAttributesCount: 0, - droppedEventsCount: 0, - droppedLinksCount: 0, - }, -]; - -export const multiResourceTrace: ReadableSpan[] = [ - { - ...basicTrace[0], - resource: mockedResources[0], - }, - { - ...basicTrace[1], - resource: mockedResources[1], - }, - { - ...basicTrace[2], - resource: mockedResources[1], - }, -]; - -export const multiInstrumentationLibraryTrace: ReadableSpan[] = [ - { - ...basicTrace[0], - instrumentationLibrary: mockedInstrumentationLibraries[0], - }, - { - ...basicTrace[1], - instrumentationLibrary: mockedInstrumentationLibraries[0], - }, - { - ...basicTrace[2], - instrumentationLibrary: mockedInstrumentationLibraries[1], - }, -]; - -export function ensureEventsAreCorrect(events: IEvent[]) { - assert.deepStrictEqual( - events, - [ - { - timeUnixNano: '1574120165429803070', - name: 'fetchStart', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'domainLookupStart', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'domainLookupEnd', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'connectStart', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'connectEnd', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165435513070', - name: 'requestStart', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165436923070', - name: 'responseStart', - attributes: [], - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165438688070', - name: 'responseEnd', - attributes: [], - droppedAttributesCount: 0, - }, - ], - 'events are incorrect' - ); -} - -export function ensureAttributesAreCorrect(attributes: IKeyValue[]) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - 'attributes are incorrect' - ); -} - -export function ensureLinksAreCorrect(attributes: ILink[], useHex?: boolean) { - assert.deepStrictEqual( - attributes, - [ - { - traceId: useHex ? traceIdHex : hexToBase64(traceIdHex), - spanId: useHex ? parentIdHex : hexToBase64(parentIdHex), - attributes: [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - droppedAttributesCount: 0, - }, - ], - 'links are incorrect' - ); -} - -export function ensureSpanIsCorrect(span: ISpan, useHex = true) { - if (span.attributes) { - ensureAttributesAreCorrect(span.attributes); - } - if (span.events) { - ensureEventsAreCorrect(span.events); - } - if (span.links) { - ensureLinksAreCorrect(span.links, useHex); - } - assert.deepStrictEqual( - span.traceId, - useHex ? traceIdHex : hexToBase64(traceIdHex), - 'traceId is' + ' wrong' - ); - assert.deepStrictEqual( - span.spanId, - useHex ? spanIdHex : hexToBase64(spanIdHex), - 'spanId is' + ' wrong' - ); - assert.deepStrictEqual( - span.parentSpanId, - useHex ? parentIdHex : hexToBase64(parentIdHex), - 'parentIdArr is wrong' - ); - assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); - assert.strictEqual(span.kind, ESpanKind.SPAN_KIND_INTERNAL, 'kind is wrong'); - assert.deepStrictEqual( - span.startTimeUnixNano, - '1574120165429803070', - 'startTimeUnixNano is wrong' - ); - assert.deepStrictEqual( - span.endTimeUnixNano, - '1574120165438688070', - 'endTimeUnixNano is wrong' - ); - assert.strictEqual( - span.droppedAttributesCount, - 0, - 'droppedAttributesCount is wrong' - ); - assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); - assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); - assert.deepStrictEqual( - span.status, - { code: SpanStatusCode.OK }, - 'status is wrong' - ); -} - -export function ensureWebResourceIsCorrect(resource: IResource) { - assert.strictEqual(resource.attributes.length, 7); - assert.strictEqual(resource.attributes[0].key, 'service.name'); - assert.strictEqual( - resource.attributes[0].value.stringValue, - 'unknown_service' - ); - assert.strictEqual(resource.attributes[1].key, 'telemetry.sdk.language'); - assert.strictEqual(resource.attributes[1].value.stringValue, 'webjs'); - assert.strictEqual(resource.attributes[2].key, 'telemetry.sdk.name'); - assert.strictEqual(resource.attributes[2].value.stringValue, 'opentelemetry'); - assert.strictEqual(resource.attributes[3].key, 'telemetry.sdk.version'); - assert.strictEqual(resource.attributes[3].value.stringValue, VERSION); - assert.strictEqual(resource.attributes[4].key, 'service'); - assert.strictEqual(resource.attributes[4].value.stringValue, 'ui'); - assert.strictEqual(resource.attributes[5].key, 'version'); - assert.strictEqual(resource.attributes[5].value.intValue, 1); - assert.strictEqual(resource.attributes[6].key, 'cost'); - assert.strictEqual(resource.attributes[6].value.doubleValue, 112.12); - assert.strictEqual(resource.droppedAttributesCount, 0); -} - -export function ensureExportTraceServiceRequestIsSet( - json: IExportTraceServiceRequest -) { - const resourceSpans = json.resourceSpans; - assert.strictEqual(resourceSpans?.length, 1, 'resourceSpans is missing'); - - const resource = resourceSpans?.[0].resource; - assert.ok(resource, 'resource is missing'); - - const scopeSpans = resourceSpans?.[0].scopeSpans; - assert.strictEqual(scopeSpans?.length, 1, 'scopeSpans is missing'); - - const scope = scopeSpans?.[0].scope; - assert.ok(scope, 'scope is missing'); - - const spans = scopeSpans?.[0].spans; - assert.strictEqual(spans?.length, 1, 'spans are missing'); -} - -export function ensureHeadersContain( - actual: { [key: string]: string }, - expected: { [key: string]: string } -) { - Object.entries(expected).forEach(([k, v]) => { - assert.strictEqual( - v, - actual[k], - `Expected ${actual} to contain ${k}: ${v}` - ); - }); -} diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts deleted file mode 100644 index 55dd0a8049f..00000000000 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/CollectorMetricExporter.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -/* - * 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 { - diag, - DiagLogger, - DiagLogLevel, - Counter, - Histogram, -} from '@opentelemetry/api'; -import { ExportResultCode } from '@opentelemetry/core'; -import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { OTLPMetricExporter } from '../../src/platform/browser'; -import { - collect, - ensureCounterIsCorrect, - ensureExportMetricsServiceRequestIsSet, - ensureHeadersContain, - ensureHistogramIsCorrect, - ensureObservableGaugeIsCorrect, - ensureWebResourceIsCorrect, - HISTOGRAM_AGGREGATION_VIEW, - mockCounter, - mockHistogram, - mockObservableGauge, - setUp, - shutdown, -} from '../metricsHelper'; -import { - AggregationTemporalityPreference, - OTLPMetricExporterOptions, -} from '../../src'; -import { OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; - -describe('OTLPMetricExporter - web', () => { - let collectorExporter: OTLPMetricExporter; - let stubOpen: sinon.SinonStub; - let stubBeacon: sinon.SinonStub; - let metrics: ResourceMetrics; - let debugStub: sinon.SinonStub; - let errorStub: sinon.SinonStub; - - beforeEach(async () => { - setUp([HISTOGRAM_AGGREGATION_VIEW]); - stubOpen = sinon.stub(XMLHttpRequest.prototype, 'open'); - sinon.stub(XMLHttpRequest.prototype, 'send'); - stubBeacon = sinon.stub(navigator, 'sendBeacon'); - - const counter: Counter = mockCounter(); - mockObservableGauge(observableResult => { - observableResult.observe(3, {}); - observableResult.observe(6, {}); - }, 'double-observable-gauge2'); - const histogram: Histogram = mockHistogram(); - - counter.add(1); - histogram.record(7); - histogram.record(14); - - const { resourceMetrics, errors } = await collect(); - assert.strictEqual(errors.length, 0); - metrics = resourceMetrics; - - // Need to stub/spy on the underlying logger as the "diag" instance is global - debugStub = sinon.stub(); - errorStub = sinon.stub(); - const nop = () => {}; - const diagLogger: DiagLogger = { - debug: debugStub, - error: errorStub, - info: nop, - verbose: nop, - warn: nop, - }; - diag.setLogger(diagLogger, DiagLogLevel.DEBUG); - }); - - afterEach(async () => { - await shutdown(); - sinon.restore(); - diag.disable(); - }); - - describe('export', () => { - describe('when "sendBeacon" is available', () => { - beforeEach(() => { - collectorExporter = new OTLPMetricExporter({ - url: 'http://foo.bar.com', - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }); - }); - - it('should successfully send metrics using sendBeacon', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(async () => { - const args = stubBeacon.args[0]; - const url = args[0]; - const blob: Blob = args[1]; - const body = await blob.text(); - const json = JSON.parse(body) as IExportMetricsServiceRequest; - - // The order of the metrics is not guaranteed. - const counterIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'int-counter' - ); - const observableIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'double-observable-gauge2' - ); - const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'int-histogram' - ); - - const metric1 = - json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; - const metric2 = - json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; - const metric3 = - json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; - - assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); - - ensureCounterIsCorrect( - metric1, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .startTime - ); - - assert.ok( - typeof metric2 !== 'undefined', - "second metric doesn't exist" - ); - ensureObservableGaugeIsCorrect( - metric2, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .startTime, - 6, - 'double-observable-gauge2' - ); - - assert.ok( - typeof metric3 !== 'undefined', - "third metric doesn't exist" - ); - ensureHistogramIsCorrect( - metric3, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .startTime, - [0, 100], - [0, 2, 0] - ); - - const resource = json.resourceMetrics[0].resource; - assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); - ensureWebResourceIsCorrect(resource); - - assert.strictEqual(url, 'http://foo.bar.com'); - - assert.strictEqual(stubBeacon.callCount, 1); - assert.strictEqual(stubOpen.callCount, 0); - - ensureExportMetricsServiceRequestIsSet(json); - - done(); - }); - }); - - it('should log the successful message', done => { - stubBeacon.returns(true); - - collectorExporter.export(metrics, () => {}); - - queueMicrotask(() => { - sinon.assert.calledWith(debugStub, 'SendBeacon success'); - sinon.assert.notCalled(errorStub); - - done(); - }); - }); - - it('should log the error message', done => { - stubBeacon.returns(false); - - collectorExporter.export(metrics, result => { - try { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok( - result.error, - 'Expected Error, but no Error was present on the result' - ); - assert.match(result.error?.message, /SendBeacon failed/); - done(); - } catch (e) { - done(e); - } - }); - }); - }); - - describe('when "sendBeacon" is NOT available', () => { - let server: any; - beforeEach(() => { - (window.navigator as any).sendBeacon = false; - collectorExporter = new OTLPMetricExporter({ - url: 'http://foo.bar.com', - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }); - // 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, () => {}); - - queueMicrotask(async () => { - try { - 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( - new TextDecoder().decode(body) - ) as IExportMetricsServiceRequest; - // The order of the metrics is not guaranteed. - const counterIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'int-counter' - ); - const observableIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'double-observable-gauge2' - ); - const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'int-histogram' - ); - - const metric1 = - json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; - const metric2 = - json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; - const metric3 = - json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; - - assert.ok(typeof metric1 !== 'undefined', "metric doesn't exist"); - ensureCounterIsCorrect( - metric1, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .startTime - ); - - assert.ok( - typeof metric2 !== 'undefined', - "second metric doesn't exist" - ); - ensureObservableGaugeIsCorrect( - metric2, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .startTime, - 6, - 'double-observable-gauge2' - ); - - assert.ok( - typeof metric3 !== 'undefined', - "third metric doesn't exist" - ); - ensureHistogramIsCorrect( - metric3, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .startTime, - [0, 100], - [0, 2, 0] - ); - - const resource = json.resourceMetrics[0].resource; - assert.ok( - typeof resource !== 'undefined', - "resource doesn't exist" - ); - ensureWebResourceIsCorrect(resource); - - assert.strictEqual(stubBeacon.callCount, 0); - ensureExportMetricsServiceRequestIsSet(json); - - done(); - } catch (e) { - done(e); - } - }); - }); - - it('should log the successful message', done => { - collectorExporter.export(metrics, () => {}); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(200); - - const response: any = debugStub.args[2][0]; - assert.strictEqual(response, 'XHR success'); - assert.strictEqual(errorStub.args.length, 0); - - assert.strictEqual(stubBeacon.callCount, 0); - done(); - }); - }); - - it('should log the error message', done => { - collectorExporter.export(metrics, result => { - try { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.deepStrictEqual( - result.error?.message, - 'XHR request failed with non-retryable status' - ); - } catch (e) { - done(e); - } - done(); - }); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(400); - }); - }); - it('should send custom headers', done => { - collectorExporter.export(metrics, () => {}); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(200); - - assert.strictEqual(stubBeacon.callCount, 0); - done(); - }); - }); - }); - }); - - describe('export with custom headers', () => { - let server: any; - const customHeaders = { - foo: 'bar', - bar: 'baz', - }; - let collectorExporterConfig: - | (OTLPExporterConfigBase & OTLPMetricExporterOptions) - | undefined; - - beforeEach(() => { - collectorExporterConfig = { - headers: customHeaders, - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }; - server = sinon.fakeServer.create(); - }); - - afterEach(() => { - server.restore(); - }); - - describe('when "sendBeacon" is available', () => { - beforeEach(() => { - collectorExporter = new OTLPMetricExporter(collectorExporterConfig); - }); - it('should successfully send custom headers using XMLHTTPRequest', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(() => { - const [{ requestHeaders }] = server.requests; - - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); - - done(); - }); - }); - }); - - describe('when "sendBeacon" is NOT available', () => { - beforeEach(() => { - (window.navigator as any).sendBeacon = false; - collectorExporter = new OTLPMetricExporter(collectorExporterConfig); - }); - - it('should successfully send metrics using XMLHttpRequest', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(() => { - const [{ requestHeaders }] = server.requests; - - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); - - done(); - }); - }); - }); - }); -}); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/OTLPMetricExporter.test.ts new file mode 100644 index 00000000000..cd6d80d8eaa --- /dev/null +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/browser/OTLPMetricExporter.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { + MeterProvider, + PeriodicExportingMetricReader, +} from '@opentelemetry/sdk-metrics'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { OTLPMetricExporter } from '../../src/platform/browser'; + +/* + * NOTE: Tests here are not intended to test the underlying components directly. They are intended as a quick + * check if the correct components are used. Use the following packages to test details: + * - `@opentelemetry/oltp-exporter-base`: OTLP common exporter logic (handling of concurrent exports, ...), HTTP transport code + * - `@opentelemetry/otlp-transformer`: Everything regarding serialization and transforming internal representations to OTLP + */ + +describe('OTLPMetricExporter', function () { + afterEach(() => { + sinon.restore(); + }); + + describe('export', function () { + describe('when sendBeacon is available', function () { + it('should successfully send data using sendBeacon', async function () { + // arrange + const stubBeacon = sinon.stub(navigator, 'sendBeacon'); + const meterProvider = new MeterProvider({ + readers: [ + new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + }), + ], + }); + + // act + meterProvider + .getMeter('test-meter') + .createCounter('test-counter') + .add(1); + await meterProvider.forceFlush(); + await meterProvider.shutdown(); + + // assert + const args = stubBeacon.args[0]; + const blob: Blob = args[1] as unknown as Blob; + const body = await blob.text(); + assert.doesNotThrow( + () => JSON.parse(body), + 'expected requestBody to be in JSON format, but parsing failed' + ); + }); + }); + + describe('when sendBeacon is not available', function () { + beforeEach(function () { + // fake sendBeacon not being available + (window.navigator as any).sendBeacon = false; + }); + + it('should successfully send data using XMLHttpRequest', async function () { + // arrange + const server = sinon.fakeServer.create(); + server.respondWith('OK'); + server.respondImmediately = true; + server.autoRespond = true; + const meterProvider = new MeterProvider({ + readers: [ + new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + }), + ], + }); + + // act + meterProvider + .getMeter('test-meter') + .createCounter('test-counter') + .add(1); + + await meterProvider.forceFlush(); + await meterProvider.shutdown(); + + // assert + const request = server.requests[0]; + const body = request.requestBody as unknown as Uint8Array; + assert.doesNotThrow( + () => JSON.parse(new TextDecoder().decode(body)), + 'expected requestBody to be in JSON format, but parsing failed' + ); + }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts deleted file mode 100644 index 850cdae4d0f..00000000000 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/metricsHelper.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* - * 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 { - Counter, - ObservableResult, - Histogram, - ValueType, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - HrTime, -} from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; -import * as assert from 'assert'; -import { InstrumentationScope, VERSION } from '@opentelemetry/core'; -import { - ExplicitBucketHistogramAggregation, - MeterProvider, - MetricReader, - View, -} from '@opentelemetry/sdk-metrics'; -import { - encodeAsString, - IExportMetricsServiceRequest, - IKeyValue, - IMetric, - IResource, -} from '@opentelemetry/otlp-transformer'; - -if (typeof Buffer === 'undefined') { - (window as any).Buffer = { - from: function (arr: []) { - return new Uint8Array(arr); - }, - }; -} - -class TestMetricReader extends MetricReader { - protected onForceFlush(): Promise { - return Promise.resolve(undefined); - } - - protected onShutdown(): Promise { - return Promise.resolve(undefined); - } -} - -export const HISTOGRAM_AGGREGATION_VIEW = new View({ - aggregation: new ExplicitBucketHistogramAggregation([0, 100]), - instrumentName: 'int-histogram', -}); - -const defaultResource = Resource.default().merge( - new Resource({ - service: 'ui', - version: 1, - cost: 112.12, - }) -); - -let reader = new TestMetricReader(); -let meterProvider = new MeterProvider({ - resource: defaultResource, - readers: [reader], -}); -let meter = meterProvider.getMeter('default', '0.0.1'); - -export async function collect() { - return (await reader.collect())!; -} - -export function setUp(views?: View[]) { - reader = new TestMetricReader(); - meterProvider = new MeterProvider({ - resource: defaultResource, - views, - readers: [reader], - }); - meter = meterProvider.getMeter('default', '0.0.1'); -} - -export async function shutdown() { - await meterProvider.shutdown(); -} - -export function mockCounter(): Counter { - const name = 'int-counter'; - return meter.createCounter(name, { - description: 'sample counter description', - valueType: ValueType.INT, - }); -} - -export function mockObservableGauge( - callback: (observableResult: ObservableResult) => void, - name = 'double-observable-gauge' -): ObservableGauge { - const observableGauge = meter.createObservableGauge(name, { - description: 'sample observable gauge description', - valueType: ValueType.DOUBLE, - }); - observableGauge.addCallback(callback); - - return observableGauge; -} - -export function mockDoubleCounter(): Counter { - const name = 'double-counter'; - return meter.createCounter(name, { - description: 'sample counter description', - valueType: ValueType.DOUBLE, - }); -} - -export function mockObservableCounter( - callback: (observableResult: ObservableResult) => void, - name = 'double-observable-counter' -): ObservableCounter { - const observableCounter = meter.createObservableCounter(name, { - description: 'sample observable counter description', - valueType: ValueType.DOUBLE, - }); - observableCounter.addCallback(callback); - - return observableCounter; -} - -export function mockObservableUpDownCounter( - callback: (observableResult: ObservableResult) => void, - name = 'double-up-down-observable-counter' -): ObservableUpDownCounter { - const observableUpDownCounter = meter.createObservableUpDownCounter(name, { - description: 'sample observable up down counter description', - valueType: ValueType.DOUBLE, - }); - observableUpDownCounter.addCallback(callback); - - return observableUpDownCounter; -} - -export function mockHistogram(): Histogram { - return meter.createHistogram('int-histogram', { - description: 'sample histogram description', - valueType: ValueType.INT, - }); -} - -export const mockedResources: Resource[] = [ - new Resource({ name: 'resource 1' }), - new Resource({ name: 'resource 2' }), -]; - -export const mockedInstrumentationLibraries: InstrumentationScope[] = [ - { - name: 'lib1', - version: '0.0.1', - }, - { - name: 'lib2', - version: '0.0.2', - }, -]; - -export function ensureAttributesAreCorrect(attributes: IKeyValue[]) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - 'attributes are incorrect' - ); -} - -export function ensureWebResourceIsCorrect(resource: IResource) { - assert.strictEqual(resource.attributes.length, 7); - assert.strictEqual(resource.attributes[0].key, 'service.name'); - assert.strictEqual( - resource.attributes[0].value.stringValue, - 'unknown_service' - ); - assert.strictEqual(resource.attributes[1].key, 'telemetry.sdk.language'); - assert.strictEqual(resource.attributes[1].value.stringValue, 'webjs'); - assert.strictEqual(resource.attributes[2].key, 'telemetry.sdk.name'); - assert.strictEqual(resource.attributes[2].value.stringValue, 'opentelemetry'); - assert.strictEqual(resource.attributes[3].key, 'telemetry.sdk.version'); - assert.strictEqual(resource.attributes[3].value.stringValue, VERSION); - assert.strictEqual(resource.attributes[4].key, 'service'); - assert.strictEqual(resource.attributes[4].value.stringValue, 'ui'); - assert.strictEqual(resource.attributes[5].key, 'version'); - assert.strictEqual(resource.attributes[5].value.intValue, 1); - assert.strictEqual(resource.attributes[6].key, 'cost'); - assert.strictEqual(resource.attributes[6].value.doubleValue, 112.12); - assert.strictEqual(resource.droppedAttributesCount, 0); -} - -export function ensureCounterIsCorrect( - metric: IMetric, - time: HrTime, - startTime: HrTime -) { - assert.strictEqual(metric.name, 'int-counter'); - assert.strictEqual(metric.description, 'sample counter description'); - assert.strictEqual(metric.unit, ''); - assert.strictEqual(metric.sum?.dataPoints.length, 1); - assert.strictEqual(metric.sum?.isMonotonic, true); - assert.strictEqual(metric.sum?.aggregationTemporality, 2); - - const [dp] = metric.sum.dataPoints; - - assert.deepStrictEqual(dp.attributes, []); - assert.strictEqual(dp.asInt, 1); - assert.deepStrictEqual(dp.startTimeUnixNano, encodeAsString(startTime)); - assert.deepStrictEqual(dp.timeUnixNano, encodeAsString(time)); -} - -export function ensureDoubleCounterIsCorrect( - metric: IMetric, - time: number, - endTime: number -) { - assert.deepStrictEqual(metric, { - name: 'double-counter', - description: 'sample counter description', - unit: '', - doubleSum: { - dataPoints: [ - { - labels: [], - value: 8, - startTimeUnixNano: endTime, - timeUnixNano: time, - }, - ], - isMonotonic: true, - aggregationTemporality: 2, - }, - }); -} - -export function ensureObservableGaugeIsCorrect( - metric: IMetric, - time: HrTime, - startTime: HrTime, - value: number, - name = 'double-observable-gauge' -) { - assert.strictEqual(metric.name, name); - assert.strictEqual(metric.description, 'sample observable gauge description'); - assert.strictEqual(metric.unit, ''); - assert.strictEqual(metric.gauge?.dataPoints.length, 1); - - const [dp] = metric.gauge.dataPoints; - - assert.deepStrictEqual(dp.attributes, []); - assert.strictEqual(dp.asDouble, value); - - assert.deepStrictEqual(dp.startTimeUnixNano, encodeAsString(startTime)); - assert.deepStrictEqual(dp.timeUnixNano, encodeAsString(time)); -} - -export function ensureHistogramIsCorrect( - metric: IMetric, - time: HrTime, - startTime: HrTime, - explicitBounds: (number | null)[] = [Infinity], - bucketCounts: number[] = [2, 0] -) { - assert.strictEqual(metric.name, 'int-histogram'); - assert.strictEqual(metric.description, 'sample histogram description'); - assert.strictEqual(metric.unit, ''); - assert.strictEqual(metric.histogram?.dataPoints.length, 1); - assert.strictEqual(metric.histogram?.aggregationTemporality, 2); - - const [dp] = metric.histogram.dataPoints; - - assert.deepStrictEqual(dp.attributes, []); - assert.strictEqual(dp.sum, 21); - assert.strictEqual(dp.count, 2); - assert.strictEqual(dp.min, 7); - assert.strictEqual(dp.max, 14); - assert.deepStrictEqual(dp.bucketCounts, bucketCounts); - assert.deepStrictEqual(dp.explicitBounds, explicitBounds); - - assert.deepStrictEqual(dp.startTimeUnixNano, encodeAsString(startTime)); - assert.deepStrictEqual(dp.timeUnixNano, encodeAsString(time)); -} - -export function ensureExportMetricsServiceRequestIsSet( - json: IExportMetricsServiceRequest -) { - const resourceMetrics = json.resourceMetrics; - assert.strictEqual( - resourceMetrics.length, - 1, - 'resourceMetrics has incorrect length' - ); - - const resource = resourceMetrics[0].resource; - assert.ok(resource, 'resource is missing'); - - const scopeMetrics = resourceMetrics[0].scopeMetrics; - assert.strictEqual(scopeMetrics?.length, 1, 'scopeMetrics is missing'); - - const scope = scopeMetrics[0].scope; - assert.ok(scope, 'scope is missing'); - - const metrics = resourceMetrics[0].scopeMetrics[0].metrics; - assert.strictEqual(metrics.length, 3, 'Metrics are missing'); -} - -export function ensureHeadersContain( - actual: { [key: string]: string }, - expected: { [key: string]: string } -) { - Object.entries(expected).forEach(([k, v]) => { - assert.strictEqual( - v, - actual[k], - `Expected ${actual} to contain ${k}: ${v}` - ); - }); -} diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts deleted file mode 100644 index 4b11c946cc1..00000000000 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts +++ /dev/null @@ -1,462 +0,0 @@ -/* - * 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 { diag, DiagLogger } from '@opentelemetry/api'; -import * as core from '@opentelemetry/core'; -import * as assert from 'assert'; -import * as http from 'http'; -import * as sinon from 'sinon'; -import { - AggregationTemporalityPreference, - OTLPMetricExporterOptions, -} from '../../src'; - -import { OTLPMetricExporter } from '../../src/platform/node'; -import { - collect, - ensureCounterIsCorrect, - ensureExportMetricsServiceRequestIsSet, - ensureHistogramIsCorrect, - ensureObservableGaugeIsCorrect, - HISTOGRAM_AGGREGATION_VIEW, - mockCounter, - mockHistogram, - mockObservableGauge, - setUp, - shutdown, -} from '../metricsHelper'; -import { MockedResponse } from './nodeHelpers'; -import { - Aggregation, - AggregationTemporality, - ExplicitBucketHistogramAggregation, - InstrumentType, - ResourceMetrics, -} from '@opentelemetry/sdk-metrics'; -import { PassThrough, Stream } from 'stream'; -import { - OTLPExporterError, - OTLPExporterNodeConfigBase, -} from '@opentelemetry/otlp-exporter-base'; -import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; - -let fakeRequest: PassThrough; - -const address = 'localhost:1501'; - -describe('OTLPMetricExporter - node with json over http', () => { - let collectorExporter: OTLPMetricExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase & - OTLPMetricExporterOptions; - let stubRequest: sinon.SinonStub; - let metrics: ResourceMetrics; - - beforeEach(async () => { - setUp([HISTOGRAM_AGGREGATION_VIEW]); - }); - - afterEach(async () => { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - await shutdown(); - sinon.restore(); - }); - - describe('instance', () => { - let warnStub: sinon.SinonStub; - - beforeEach(() => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - warnStub = sinon.stub(); - const nop = () => {}; - const diagLogger: DiagLogger = { - debug: nop, - error: nop, - info: nop, - verbose: nop, - warn: warnStub, - }; - diag.setLogger(diagLogger); - }); - - afterEach(() => { - diag.disable(); - }); - - it('should warn about metadata when using json', () => { - const metadata = 'foo'; - collectorExporter = new OTLPMetricExporter({ - url: address, - metadata, - } as any); - const args = warnStub.args[0]; - assert.strictEqual(args[0], 'Metadata cannot be set when using http'); - }); - }); - - describe('temporality', () => { - it('should use the right temporality when Cumulative preference is selected', () => { - const exporter = new OTLPMetricExporter({ - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }); - - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.COUNTER), - AggregationTemporality.CUMULATIVE, - 'Counter' - ); - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM), - AggregationTemporality.CUMULATIVE, - 'Histogram' - ); - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER), - AggregationTemporality.CUMULATIVE, - 'UpDownCounter' - ); - assert.equal( - exporter.selectAggregationTemporality( - InstrumentType.OBSERVABLE_COUNTER - ), - AggregationTemporality.CUMULATIVE, - 'Asynchronous Counter' - ); - assert.equal( - exporter.selectAggregationTemporality( - InstrumentType.OBSERVABLE_UP_DOWN_COUNTER - ), - AggregationTemporality.CUMULATIVE, - 'Asynchronous UpDownCounter' - ); - }); - - it('should use the right temporality when Delta preference is selected', () => { - const exporter = new OTLPMetricExporter({ - temporalityPreference: AggregationTemporalityPreference.DELTA, - }); - - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.COUNTER), - AggregationTemporality.DELTA, - 'Counter' - ); - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM), - AggregationTemporality.DELTA, - 'Histogram' - ); - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER), - AggregationTemporality.CUMULATIVE, - 'UpDownCounter' - ); - assert.equal( - exporter.selectAggregationTemporality( - InstrumentType.OBSERVABLE_COUNTER - ), - AggregationTemporality.DELTA, - 'Asynchronous Counter' - ); - assert.equal( - exporter.selectAggregationTemporality( - InstrumentType.OBSERVABLE_UP_DOWN_COUNTER - ), - AggregationTemporality.CUMULATIVE, - 'Asynchronous UpDownCounter' - ); - }); - - it('should use the right temporality when LowMemory preference is selected', () => { - const exporter = new OTLPMetricExporter({ - temporalityPreference: AggregationTemporalityPreference.LOWMEMORY, - }); - - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.COUNTER), - AggregationTemporality.DELTA, - 'Counter' - ); - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM), - AggregationTemporality.DELTA, - 'Histogram' - ); - assert.equal( - exporter.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER), - AggregationTemporality.CUMULATIVE, - 'UpDownCounter' - ); - assert.equal( - exporter.selectAggregationTemporality( - InstrumentType.OBSERVABLE_COUNTER - ), - AggregationTemporality.CUMULATIVE, - 'Asynchronous Counter' - ); - assert.equal( - exporter.selectAggregationTemporality( - InstrumentType.OBSERVABLE_UP_DOWN_COUNTER - ), - AggregationTemporality.CUMULATIVE, - 'Asynchronous UpDownCounter' - ); - }); - }); - - describe('aggregation', () => { - it('aggregationSelector calls the selector supplied to the constructor', () => { - const aggregation = new ExplicitBucketHistogramAggregation([ - 0, 100, 100000, - ]); - const exporter = new OTLPMetricExporter({ - aggregationPreference: _instrumentType => aggregation, - }); - assert.equal( - exporter.selectAggregation(InstrumentType.COUNTER), - aggregation - ); - }); - - it('aggregationSelector returns the default aggregation preference when nothing is supplied', () => { - const exporter = new OTLPMetricExporter({ - aggregationPreference: _instrumentType => Aggregation.Default(), - }); - assert.equal( - exporter.selectAggregation(InstrumentType.COUNTER), - Aggregation.Default() - ); - }); - }); - - describe('export', () => { - beforeEach(async () => { - stubRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - httpAgentOptions: { keepAliveMsecs: 2000 }, - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }; - - collectorExporter = new OTLPMetricExporter(collectorExporterConfig); - - const counter = mockCounter(); - mockObservableGauge(observableResult => { - observableResult.observe(6, {}); - }, 'double-observable-gauge2'); - const histogram = mockHistogram(); - counter.add(1); - histogram.record(7); - histogram.record(14); - - const { resourceMetrics, errors } = await collect(); - assert.strictEqual(errors.length, 0); - metrics = resourceMetrics; - }); - - it('should open the connection', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - const options = args[0]; - - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); - done(); - }); - }); - - it('should set custom headers', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - const options = args[0]; - assert.strictEqual(options.headers['foo'], 'bar'); - done(); - }); - }); - - it('should have keep alive and keepAliveMsecs option set', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - const options = args[0]; - const agent = options.agent; - assert.strictEqual(agent.keepAlive, true); - assert.strictEqual(agent.options.keepAliveMsecs, 2000); - done(); - }); - }); - - it('should successfully send metrics', done => { - let buff = Buffer.from(''); - - collectorExporter.export(metrics, () => {}); - - fakeRequest.on('end', () => { - const responseBody = buff.toString(); - - const json = JSON.parse(responseBody) as IExportMetricsServiceRequest; - // The order of the metrics is not guaranteed. - const counterIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'int-counter' - ); - const observableIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'double-observable-gauge2' - ); - const histogramIndex = metrics.scopeMetrics[0].metrics.findIndex( - it => it.descriptor.name === 'int-histogram' - ); - - const metric1 = - json.resourceMetrics[0].scopeMetrics[0].metrics[counterIndex]; - const metric2 = - json.resourceMetrics[0].scopeMetrics[0].metrics[observableIndex]; - const metric3 = - json.resourceMetrics[0].scopeMetrics[0].metrics[histogramIndex]; - - assert.ok(typeof metric1 !== 'undefined', "counter doesn't exist"); - ensureCounterIsCorrect( - metric1, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime, - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].startTime - ); - assert.ok( - typeof metric2 !== 'undefined', - "observable gauge doesn't exist" - ); - ensureObservableGaugeIsCorrect( - metric2, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .startTime, - 6, - 'double-observable-gauge2' - ); - assert.ok(typeof metric3 !== 'undefined', "histogram doesn't exist"); - ensureHistogramIsCorrect( - metric3, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0].endTime, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .startTime, - [0, 100], - [0, 2, 0] - ); - - ensureExportMetricsServiceRequestIsSet(json); - - done(); - }); - - fakeRequest.on('data', chunk => { - buff = Buffer.concat([buff, chunk]); - }); - - const mockRes = new MockedResponse(200); - const args = stubRequest.args[0]; - const callback = args[1]; - - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - it('should log the successful message', done => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - const stubLoggerError = sinon.stub(diag, 'error'); - - const responseSpy = sinon.spy(); - collectorExporter.export(metrics, responseSpy); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - callback(mockRes); - mockRes.send(Buffer.from('success')); - }); - - setTimeout(() => { - assert.strictEqual(stubLoggerError.args.length, 0); - assert.strictEqual( - responseSpy.args[0][0].code, - core.ExportResultCode.SUCCESS - ); - done(); - }); - }); - }); - - it('should log the error message', done => { - const handler = core.loggingErrorHandler(); - core.setGlobalErrorHandler(handler); - - const responseSpy = sinon.spy(); - collectorExporter.export(metrics, responseSpy); - - setTimeout(() => { - const args = stubRequest.args[0]; - const callback = args[1]; - queueMicrotask(() => { - const mockRes = new MockedResponse(400); - callback(mockRes); - mockRes.send(Buffer.from('failure')); - }); - - setTimeout(() => { - const result = responseSpy.args[0][0] as core.ExportResult; - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.code, 400); - done(); - }); - }); - }); - }); -}); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/OTLPMetricExporter.test.ts new file mode 100644 index 00000000000..87569f327a3 --- /dev/null +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/OTLPMetricExporter.test.ts @@ -0,0 +1,221 @@ +/* + * 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 * as assert from 'assert'; +import * as http from 'http'; +import * as sinon from 'sinon'; +import { AggregationTemporalityPreference } from '../../src'; + +import { OTLPMetricExporter } from '../../src/platform/node'; +import { + Aggregation, + AggregationTemporality, + ExplicitBucketHistogramAggregation, + InstrumentType, + MeterProvider, + PeriodicExportingMetricReader, +} from '@opentelemetry/sdk-metrics'; +import { Stream } from 'stream'; + +/* + * NOTE: Tests here are not intended to test the underlying components directly. They are intended as a quick + * check if the correct components are used. Use the following packages to test details: + * - `@opentelemetry/oltp-exporter-base`: OTLP common exporter logic (handling of concurrent exports, ...), HTTP transport code + * - `@opentelemetry/otlp-transformer`: Everything regarding serialization and transforming internal representations to OTLP + */ + +describe('OTLPMetricExporter', () => { + describe('temporality', () => { + it('should use the right temporality when Cumulative preference is selected', () => { + const exporter = new OTLPMetricExporter({ + temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, + }); + + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.COUNTER), + AggregationTemporality.CUMULATIVE, + 'Counter' + ); + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM), + AggregationTemporality.CUMULATIVE, + 'Histogram' + ); + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER), + AggregationTemporality.CUMULATIVE, + 'UpDownCounter' + ); + assert.equal( + exporter.selectAggregationTemporality( + InstrumentType.OBSERVABLE_COUNTER + ), + AggregationTemporality.CUMULATIVE, + 'Asynchronous Counter' + ); + assert.equal( + exporter.selectAggregationTemporality( + InstrumentType.OBSERVABLE_UP_DOWN_COUNTER + ), + AggregationTemporality.CUMULATIVE, + 'Asynchronous UpDownCounter' + ); + }); + + it('should use the right temporality when Delta preference is selected', () => { + const exporter = new OTLPMetricExporter({ + temporalityPreference: AggregationTemporalityPreference.DELTA, + }); + + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.COUNTER), + AggregationTemporality.DELTA, + 'Counter' + ); + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM), + AggregationTemporality.DELTA, + 'Histogram' + ); + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER), + AggregationTemporality.CUMULATIVE, + 'UpDownCounter' + ); + assert.equal( + exporter.selectAggregationTemporality( + InstrumentType.OBSERVABLE_COUNTER + ), + AggregationTemporality.DELTA, + 'Asynchronous Counter' + ); + assert.equal( + exporter.selectAggregationTemporality( + InstrumentType.OBSERVABLE_UP_DOWN_COUNTER + ), + AggregationTemporality.CUMULATIVE, + 'Asynchronous UpDownCounter' + ); + }); + + it('should use the right temporality when LowMemory preference is selected', () => { + const exporter = new OTLPMetricExporter({ + temporalityPreference: AggregationTemporalityPreference.LOWMEMORY, + }); + + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.COUNTER), + AggregationTemporality.DELTA, + 'Counter' + ); + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.HISTOGRAM), + AggregationTemporality.DELTA, + 'Histogram' + ); + assert.equal( + exporter.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER), + AggregationTemporality.CUMULATIVE, + 'UpDownCounter' + ); + assert.equal( + exporter.selectAggregationTemporality( + InstrumentType.OBSERVABLE_COUNTER + ), + AggregationTemporality.CUMULATIVE, + 'Asynchronous Counter' + ); + assert.equal( + exporter.selectAggregationTemporality( + InstrumentType.OBSERVABLE_UP_DOWN_COUNTER + ), + AggregationTemporality.CUMULATIVE, + 'Asynchronous UpDownCounter' + ); + }); + }); + + describe('aggregation', () => { + it('aggregationSelector calls the selector supplied to the constructor', () => { + const aggregation = new ExplicitBucketHistogramAggregation([ + 0, 100, 100000, + ]); + const exporter = new OTLPMetricExporter({ + aggregationPreference: _instrumentType => aggregation, + }); + assert.equal( + exporter.selectAggregation(InstrumentType.COUNTER), + aggregation + ); + }); + + it('aggregationSelector returns the default aggregation preference when nothing is supplied', () => { + const exporter = new OTLPMetricExporter({ + aggregationPreference: _instrumentType => Aggregation.Default(), + }); + assert.equal( + exporter.selectAggregation(InstrumentType.COUNTER), + Aggregation.Default() + ); + }); + }); + + describe('export', () => { + afterEach(() => { + sinon.restore(); + }); + + it('successfully exports data', function (done) { + // arrange + const fakeRequest = new Stream.PassThrough(); + Object.defineProperty(fakeRequest, 'setTimeout', { + value: function (_timeout: number) {}, + }); + + sinon.stub(http, 'request').returns(fakeRequest as any); + let buff = Buffer.from(''); + fakeRequest.on('finish', () => { + try { + // assert + const requestBody = buff.toString(); + assert.doesNotThrow(() => { + JSON.parse(requestBody); + }, 'expected requestBody to be in JSON format, but parsing failed'); + done(); + } catch (e) { + done(e); + } + }); + + fakeRequest.on('data', chunk => { + buff = Buffer.concat([buff, chunk]); + }); + + const meterProvider = new MeterProvider({ + readers: [ + new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + }), + ], + }); + meterProvider.getMeter('test-meter').createCounter('test-counter').add(1); + + // act + meterProvider.forceFlush(); + meterProvider.shutdown(); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/nodeHelpers.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/nodeHelpers.ts deleted file mode 100644 index e63d21b17c9..00000000000 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/nodeHelpers.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { Stream } from 'stream'; - -export class MockedResponse extends Stream { - constructor( - private _code: number, - private _msg?: string - ) { - super(); - } - - send(data: Uint8Array) { - this.emit('data', data); - this.emit('end'); - } - - get statusCode() { - return this._code; - } - - get statusMessage() { - return this._msg; - } -}