diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 7edb7aa07e1..2fca9156dc9 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -32,6 +32,8 @@ All notable changes to experimental packages in this project will be documented ### :house: (Internal) +* chore(otlp-exporter-\*-proto): clean up tests [#5196](https://github.com/open-telemetry/opentelemetry-js/pull/5199) @pichlermarc + ## 0.55.0 ### :boom: Breaking Change diff --git a/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts index 29ed48ff4a8..dc6ffc2665c 100644 --- a/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts @@ -13,26 +13,82 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import * as assert from 'assert'; import * as sinon from 'sinon'; -import { OTLPLogExporter } from '../../src/platform/browser/index'; - -describe('OTLPLogExporter - web', () => { - let collectorLogsExporter: OTLPLogExporter; - describe('constructor', () => { - beforeEach(() => { - const collectorExporterConfig = { - hostname: 'foo', - url: 'http://foo.bar.com', - }; - collectorLogsExporter = new OTLPLogExporter(collectorExporterConfig); - }); - afterEach(() => { - sinon.restore(); + +import { OTLPLogExporter } from '../../src/platform/browser'; +import { + LoggerProvider, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; + +/* + * 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('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.throws( + () => JSON.parse(body), + 'expected requestBody to be in protobuf format, but parsing as JSON succeeded' + ); + }); }); - it('should create an instance', () => { - assert.ok(typeof collectorLogsExporter !== 'undefined'); + + 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.throws( + () => JSON.parse(new TextDecoder().decode(body)), + 'expected requestBody to be in protobuf format, but parsing as JSON succeeded' + ); + }); }); }); }); diff --git a/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts b/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts deleted file mode 100644 index c5a110ca1ac..00000000000 --- a/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts +++ /dev/null @@ -1,190 +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'; -import { Stream } from 'stream'; - -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.strictEqual( - logRecord.timeUnixNano, - '1680253513123241635', - 'timeUnixNano is wrong' - ); - assert.strictEqual( - logRecord.observedTimeUnixNano, - '1680253513123241635', - 'observedTimeUnixNano is wrong' - ); - assert.strictEqual( - logRecord.severityNumber, - 'SEVERITY_NUMBER_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'); -} - -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-logs-otlp-proto/test/node/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts index 475b00ad741..7bb6b9ee4e3 100644 --- a/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts @@ -14,333 +14,61 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; -import { ExportResultCode } from '@opentelemetry/core'; import * as assert from 'assert'; import * as http from 'http'; import * as sinon from 'sinon'; -import { Stream, PassThrough } from 'stream'; -import * as zlib from 'zlib'; -import { OTLPLogExporter } from '../../src'; -import { - ensureExportLogsServiceRequestIsSet, - ensureExportedLogRecordIsCorrect, - mockedReadableLogRecord, - MockedResponse, -} from '../logHelper'; -import { - CompressionAlgorithm, - OTLPExporterNodeConfigBase, - OTLPExporterError, -} from '@opentelemetry/otlp-exporter-base'; -import { IExportLogsServiceRequest } from '@opentelemetry/otlp-transformer'; -import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; -import { Root } from 'protobufjs'; -import * as path from 'path'; - -let fakeRequest: PassThrough; -const dir = path.resolve(__dirname, '../../../otlp-transformer/protos'); -const root = new Root(); -root.resolvePath = function (origin, target) { - return `${dir}/${target}`; -}; -const proto = root.loadSync([ - 'opentelemetry/proto/common/v1/common.proto', - 'opentelemetry/proto/resource/v1/resource.proto', - 'opentelemetry/proto/logs/v1/logs.proto', - 'opentelemetry/proto/collector/logs/v1/logs_service.proto', -]); -const exportRequestServiceProto = proto?.lookupType('ExportLogsServiceRequest'); - -describe('OTLPLogExporter - node with proto over http', () => { - let collectorExporter: OTLPLogExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let logs: ReadableLogRecord[]; +import { OTLPLogExporter } from '../../src/platform/node'; +import { + LoggerProvider, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; +import { Stream } from 'stream'; - afterEach(() => { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - sinon.restore(); - }); +/* + * 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', () => { 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) {}, }); - sinon.stub(http, 'request').returns(fakeRequest as any); - - let buff = Buffer.from(''); - fakeRequest.on('end', () => { - const data = exportRequestServiceProto.decode(buff); - const json = data?.toJSON() 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(); - }); - 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; - }); - - 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(); - }); - }); - }); - describe('export - with compression', () => { - beforeEach(() => { - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - compression: CompressionAlgorithm.GZIP, - httpAgentOptions: { keepAliveMsecs: 2000 }, - }; - collectorExporter = new OTLPLogExporter(collectorExporterConfig); - logs = []; - logs.push(Object.assign({}, mockedReadableLogRecord)); - }); - afterEach(() => { - sinon.restore(); - }); - - it('should successfully send the logs', done => { - const fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); sinon.stub(http, 'request').returns(fakeRequest as any); - const spySetHeader = sinon.spy(); - (fakeRequest as any).setHeader = spySetHeader; - let buff = Buffer.from(''); - fakeRequest.on('end', () => { - const unzippedBuff = zlib.gunzipSync(buff); - const data = exportRequestServiceProto.decode(unzippedBuff); - const json = data?.toJSON() as IExportLogsServiceRequest; - const log1 = json.resourceLogs?.[0].scopeLogs?.[0].logRecords?.[0]; - assert.ok(typeof log1 !== 'undefined', "log doesn't exist"); - ensureExportedLogRecordIsCorrect(log1); - - ensureExportLogsServiceRequestIsSet(json); - assert.ok(spySetHeader.calledWith('Content-Encoding', 'gzip')); - - done(); + fakeRequest.on('finish', () => { + try { + const requestBody = buff.toString(); + assert.throws(() => { + JSON.parse(requestBody); + }, 'expected requestBody to be in protobuf format, but parsing as JSON succeeded'); + 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(); - }); - }); -}); - -describe('export - real http request destroyed before response received', () => { - let collectorExporter: OTLPLogExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let logs: ReadableLogRecord[]; - const server = http.createServer((_, res) => { - setTimeout(() => { - res.statusCode = 200; - res.end(); - }, 1000); - }); - before(done => { - server.listen(8082, done); - }); - after(done => { - server.close(done); - }); - it('should log the timeout request error message when timeout is 1', done => { - collectorExporterConfig = { - url: 'http://localhost:8082', - timeoutMillis: 1, - }; - collectorExporter = new OTLPLogExporter(collectorExporterConfig); - logs = []; - logs.push(Object.assign({}, mockedReadableLogRecord)); - - collectorExporter.export(logs, result => { - try { - assert.strictEqual(result.code, ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); - } catch (e) { - done(e); - } - }); - }); - it('should log the timeout request error message when timeout is 100', done => { - collectorExporterConfig = { - url: 'http://localhost:8082', - timeoutMillis: 100, - }; - collectorExporter = new OTLPLogExporter(collectorExporterConfig); - logs = []; - logs.push(Object.assign({}, mockedReadableLogRecord)); - - collectorExporter.export(logs, result => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); - }); - }); -}); - -describe('export - real http request destroyed after response received', () => { - let collectorExporter: OTLPLogExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let logs: ReadableLogRecord[]; - - const server = http.createServer((_, res) => { - res.write('writing something'); - }); - before(done => { - server.listen(8082, done); - }); - after(done => { - server.close(done); - }); - it('should log the timeout request error message', done => { - collectorExporterConfig = { - url: 'http://localhost:8082', - timeoutMillis: 300, - }; - collectorExporter = new OTLPLogExporter(collectorExporterConfig); - logs = []; - logs.push(Object.assign({}, mockedReadableLogRecord)); + const loggerProvider = new LoggerProvider(); + loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new OTLPLogExporter()) + ); - collectorExporter.export(logs, result => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); + loggerProvider.getLogger('test-logger').emit({ body: 'test-body' }); + loggerProvider.shutdown(); }); }); }); diff --git a/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts deleted file mode 100644 index 264d0051f52..00000000000 --- a/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts +++ /dev/null @@ -1,38 +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 sinon from 'sinon'; -import { OTLPTraceExporter } from '../../src/platform/browser/index'; - -describe('OTLPTraceExporter - web', () => { - let collectorTraceExporter: OTLPTraceExporter; - describe('constructor', () => { - beforeEach(() => { - const collectorExporterConfig = { - hostname: 'foo', - url: 'http://foo.bar.com', - }; - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - afterEach(() => { - sinon.restore(); - }); - it('should create an instance', () => { - assert.ok(typeof collectorTraceExporter !== 'undefined'); - }); - }); -}); diff --git a/experimental/packages/exporter-trace-otlp-proto/test/browser/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-proto/test/browser/OTLPTraceExporter.test.ts new file mode 100644 index 00000000000..24e517c8b75 --- /dev/null +++ b/experimental/packages/exporter-trace-otlp-proto/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.throws( + () => JSON.parse(body), + 'expected requestBody to be in protobuf format, but parsing as JSON succeeded' + ); + }); + }); + + 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.throws( + () => JSON.parse(new TextDecoder().decode(body)), + 'expected requestBody to be in protobuf format, but parsing as JSON succeeded' + ); + }); + }); + }); +}); diff --git a/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts index 355dacac213..c4eeb56d59f 100644 --- a/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts @@ -14,328 +14,60 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; -import { ExportResultCode } from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; import * as http from 'http'; import * as sinon from 'sinon'; -import { Stream, PassThrough } from 'stream'; -import * as zlib from 'zlib'; -import { OTLPTraceExporter } from '../../src'; -import { - ensureExportTraceServiceRequestIsSet, - ensureProtoSpanIsCorrect, - mockedReadableSpan, - MockedResponse, -} from '../traceHelper'; -import { - CompressionAlgorithm, - OTLPExporterNodeConfigBase, - OTLPExporterError, -} from '@opentelemetry/otlp-exporter-base'; -import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; -import { Root } from 'protobufjs'; -import * as path from 'path'; - -const dir = path.resolve(__dirname, '../../../otlp-transformer/protos'); -const root = new Root(); -root.resolvePath = function (origin, target) { - return `${dir}/${target}`; -}; -const proto = root.loadSync([ - 'opentelemetry/proto/common/v1/common.proto', - 'opentelemetry/proto/resource/v1/resource.proto', - 'opentelemetry/proto/trace/v1/trace.proto', - 'opentelemetry/proto/collector/trace/v1/trace_service.proto', -]); -const exportRequestServiceProto = proto?.lookupType( - 'ExportTraceServiceRequest' -); +import { Stream } from 'stream'; -let fakeRequest: PassThrough; - -describe('OTLPTraceExporter - node with proto over http', () => { - let collectorExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let spans: ReadableSpan[]; +import { + BasicTracerProvider, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { OTLPTraceExporter } from '../../src/platform/node'; - afterEach(() => { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - sinon.restore(); - }); +/* + * 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', () => { - beforeEach(() => { - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - httpAgentOptions: { keepAliveMsecs: 2000 }, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); afterEach(() => { sinon.restore(); }); - it('should open the connection', done => { - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - try { - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); - } catch (e) { - done(e); - } - - queueMicrotask(() => { - const mockRes = new MockedResponse(200); - cb(mockRes); - mockRes.send(Buffer.from('success')); - done(); - }); - return fakeRequest as any; - }); - - collectorExporter.export(spans, () => {}); - }); - - 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(spans, () => {}); - }); - - 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(spans, () => {}); - }); - - it('should successfully send the spans', done => { + 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); + sinon.stub(http, 'request').returns(fakeRequest as any); let buff = Buffer.from(''); fakeRequest.on('finish', () => { - const data = exportRequestServiceProto.decode(buff); - const json = data?.toJSON() as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureProtoSpanIsCorrect(span1); - - ensureExportTraceServiceRequestIsSet(json); - - done(); - }); - - fakeRequest.on('data', chunk => { - buff = Buffer.concat([buff, chunk]); - }); - - const clock = sinon.useFakeTimers(); - collectorExporter.export(spans, () => {}); - 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; - }); - - collectorExporter.export(spans, result => { try { - assert.strictEqual(result.code, ExportResultCode.SUCCESS); - assert.strictEqual(spyLoggerError.args.length, 0); + const requestBody = buff.toString(); + assert.throws(() => { + JSON.parse(requestBody); + }, 'expected requestBody to be in protobuf format, but parsing as JSON succeeded'); done(); } catch (e) { done(e); } }); - }); - - 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(spans, result => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - // @ts-expect-error verify error code - assert.strictEqual(result.error.code, 400); - done(); - }); - }); - }); - describe('export - with compression', () => { - beforeEach(() => { - 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)); - }); - afterEach(() => { - sinon.restore(); - }); - - it('should successfully send the spans', done => { - const fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - sinon.stub(http, 'request').returns(fakeRequest as any); - const spySetHeader = sinon.spy(); - (fakeRequest as any).setHeader = spySetHeader; - - let buff = Buffer.from(''); - fakeRequest.on('finish', () => { - const unzippedBuff = zlib.gunzipSync(buff); - const data = exportRequestServiceProto.decode(unzippedBuff); - const json = data?.toJSON() as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureProtoSpanIsCorrect(span1); - - ensureExportTraceServiceRequestIsSet(json); - assert.ok(spySetHeader.calledWith('Content-Encoding', 'gzip')); - - done(); - }); fakeRequest.on('data', chunk => { buff = Buffer.concat([buff, chunk]); }); - const clock = sinon.useFakeTimers(); - collectorExporter.export(spans, () => {}); - clock.tick(200); - clock.restore(); - }); - }); -}); - -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(); - }, 1000); - }); - before(done => { - server.listen(8080, done); - }); - after(done => { - server.close(done); - }); - it('should log the timeout request error message when timeout is 100', done => { - collectorExporterConfig = { - url: 'http://localhost:8080', - timeoutMillis: 100, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - collectorExporter.export(spans, result => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); - }); - }); -}); - -describe('export - real http request destroyed after response received', () => { - let collectorExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let spans: ReadableSpan[]; - - const server = http.createServer((_, res) => { - res.write('writing something'); - }); - before(done => { - server.listen(8080, done); - }); - after(done => { - server.close(done); - }); - it('should log the timeout request error message', done => { - collectorExporterConfig = { - url: 'http://localhost:8080', - timeoutMillis: 300, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - collectorExporter.export(spans, result => { - assert.strictEqual(result.code, ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); + new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter())], + }) + .getTracer('test-tracer') + .startSpan('test-span') + .end(); }); }); }); diff --git a/experimental/packages/exporter-trace-otlp-proto/test/traceHelper.ts b/experimental/packages/exporter-trace-otlp-proto/test/traceHelper.ts deleted file mode 100644 index efbdc032830..00000000000 --- a/experimental/packages/exporter-trace-otlp-proto/test/traceHelper.ts +++ /dev/null @@ -1,283 +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 } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import * as assert from 'assert'; -import { Stream } from 'stream'; -import { - IEvent, - IExportTraceServiceRequest, - IKeyValue, - ILink, - ISpan, -} from '@opentelemetry/otlp-transformer'; - -const traceIdHex = '1f1008dc8e270e85c40a0d7c3939b278'; -const spanIdHex = '5e107261f64fa53e'; -const parentIdHex = '78a8915098864388'; - -export const mockedReadableSpan: ReadableSpan = { - name: 'documentFetch', - kind: 0, - spanContext: () => { - return { - traceId: traceIdHex, - spanId: spanIdHex, - traceFlags: TraceFlags.SAMPLED, - }; - }, - parentSpanId: parentIdHex, - startTime: [1574120165, 429803070], - endTime: [1574120165, 438688070], - ended: true, - status: { code: SpanStatusCode.OK }, - attributes: { component: 'document-load' }, - links: [ - { - context: { - traceId: traceIdHex, - spanId: parentIdHex, - 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: new Resource({ - service: 'ui', - version: 1, - cost: 112.12, - }), - instrumentationLibrary: { name: 'default', version: '0.0.1' }, - droppedAttributesCount: 0, - droppedEventsCount: 0, - droppedLinksCount: 0, -}; - -export function ensureProtoEventsAreCorrect(events: IEvent[]) { - assert.deepStrictEqual( - events, - [ - { - timeUnixNano: '1574120165429803070', - name: 'fetchStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'domainLookupStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'domainLookupEnd', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'connectStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165429803070', - name: 'connectEnd', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165435513070', - name: 'requestStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165436923070', - name: 'responseStart', - droppedAttributesCount: 0, - }, - { - timeUnixNano: '1574120165438688070', - name: 'responseEnd', - droppedAttributesCount: 0, - }, - ], - 'events are incorrect' - ); -} - -export function ensureProtoAttributesAreCorrect(attributes: IKeyValue[]) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - 'attributes are incorrect' - ); -} - -export function ensureProtoLinksAreCorrect(attributes: ILink[]) { - assert.deepStrictEqual( - attributes, - [ - { - traceId: hexToBase64(traceIdHex), - spanId: hexToBase64(parentIdHex), - attributes: [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - droppedAttributesCount: 0, - }, - ], - 'links are incorrect' - ); -} - -export function ensureProtoSpanIsCorrect(span: ISpan) { - if (span.attributes) { - ensureProtoAttributesAreCorrect(span.attributes); - } - if (span.events) { - ensureProtoEventsAreCorrect(span.events); - } - if (span.links) { - ensureProtoLinksAreCorrect(span.links); - } - assert.deepStrictEqual( - span.traceId, - hexToBase64(traceIdHex), - 'traceId is' + ' wrong' - ); - assert.deepStrictEqual( - span.spanId, - hexToBase64(spanIdHex), - 'spanId is' + ' wrong' - ); - assert.deepStrictEqual( - span.parentSpanId, - hexToBase64(parentIdHex), - 'parentIdArr is wrong' - ); - assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); - assert.strictEqual(span.kind, 'SPAN_KIND_INTERNAL', 'kind is wrong'); - assert.strictEqual( - span.startTimeUnixNano, - '1574120165429803070', - 'startTimeUnixNano is wrong' - ); - assert.strictEqual( - 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: 'STATUS_CODE_OK' }, - 'status is wrong' - ); -} - -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 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/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts index d8dcdc72b62..aeb7e14ecbf 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts @@ -14,222 +14,46 @@ * limitations under the License. */ -import { diag } from '@opentelemetry/api'; -import { ExportResultCode } from '@opentelemetry/core'; import * as assert from 'assert'; import * as http from 'http'; import * as sinon from 'sinon'; -import { OTLPMetricExporter } from '../src'; +import { OTLPMetricExporter } from '../src/'; import { - ensureExportedCounterIsCorrect, - ensureExportedObservableGaugeIsCorrect, - ensureExportedHistogramIsCorrect, - ensureExportMetricsServiceRequestIsSet, - mockCounter, - MockedResponse, - mockObservableGauge, - mockHistogram, - collect, - setUp, - shutdown, -} from './metricsHelper'; -import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; -import { - AggregationTemporalityPreference, - OTLPMetricExporterOptions, -} from '@opentelemetry/exporter-metrics-otlp-http'; -import { Stream, PassThrough } from 'stream'; -import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { Root } from 'protobufjs'; -import * as path from 'path'; - -let fakeRequest: PassThrough; - -const dir = path.resolve(__dirname, '../../otlp-transformer/protos'); -const root = new Root(); -root.resolvePath = function (origin, target) { - return `${dir}/${target}`; -}; -const proto = root.loadSync([ - 'opentelemetry/proto/common/v1/common.proto', - 'opentelemetry/proto/resource/v1/resource.proto', - 'opentelemetry/proto/metrics/v1/metrics.proto', - 'opentelemetry/proto/collector/metrics/v1/metrics_service.proto', -]); -const exportRequestServiceProto = proto?.lookupType( - 'ExportMetricsServiceRequest' -); - -describe('OTLPMetricExporter - node with proto over http', () => { - let collectorExporter: OTLPMetricExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase & - OTLPMetricExporterOptions; - let metrics: ResourceMetrics; + MeterProvider, + PeriodicExportingMetricReader, +} from '@opentelemetry/sdk-metrics'; +import { Stream } from 'stream'; - afterEach(() => { - fakeRequest = new Stream.PassThrough(); - Object.defineProperty(fakeRequest, 'setTimeout', { - value: function (_timeout: number) {}, - }); - sinon.restore(); - }); +/* + * 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('export', () => { - beforeEach(async () => { - collectorExporterConfig = { - headers: { - foo: 'bar', - }, - url: 'http://foo.bar.com', - keepAlive: true, - httpAgentOptions: { keepAliveMsecs: 2000 }, - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }; - collectorExporter = new OTLPMetricExporter(collectorExporterConfig); - setUp(); - - const counter = mockCounter(); - mockObservableGauge(observableResult => { - observableResult.observe(3, {}); - observableResult.observe(6, {}); - }); - const histogram = mockHistogram(); - - counter.add(1); - histogram.record(7); - histogram.record(14); - - const { resourceMetrics, errors } = await collect(); - assert.strictEqual(errors.length, 0); - metrics = resourceMetrics; - }); - - afterEach(async () => { - await shutdown(); + 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(metrics, () => {}); - }); - - 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(metrics, () => {}); - }); - - it('should have keep alive and keepAliveMsecs option set', done => { - sinon.stub(http, 'request').callsFake((options: any, cb: any) => { - try { - 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(); - }); - } catch (e) { - done(e); - } - return fakeRequest as any; - }); - - collectorExporter.export(metrics, () => {}); - }); - - it('should successfully send metrics', done => { + 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); + sinon.stub(http, 'request').returns(fakeRequest as any); let buff = Buffer.from(''); - fakeRequest.on('finish', () => { try { - const data = exportRequestServiceProto.decode(buff); - const json = data?.toJSON() as any; - - // 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-gauge' - ); - 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"); - ensureExportedCounterIsCorrect( - 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" - ); - ensureExportedObservableGaugeIsCorrect( - metric2, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .startTime - ); - assert.ok( - typeof metric3 !== 'undefined', - "value recorder doesn't exist" - ); - ensureExportedHistogramIsCorrect( - metric3, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .endTime, - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .startTime, - [0, 100], - ['0', '2', '0'] - ); - - ensureExportMetricsServiceRequestIsSet(json); + // assert + const requestBody = buff.toString(); + assert.throws(() => { + JSON.parse(requestBody); + }, 'expected requestBody to be in protobuf format, but parsing as JSON succeeded'); done(); } catch (e) { done(e); @@ -240,55 +64,18 @@ describe('OTLPMetricExporter - node with proto over http', () => { buff = Buffer.concat([buff, chunk]); }); - try { - collectorExporter.export(metrics, () => {}); - } catch (error) { - done(error); - } - }); - - 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; - }); - - collectorExporter.export(metrics, result => { - assert.strictEqual(result.code, ExportResultCode.SUCCESS); - sinon.assert.notCalled(spyLoggerError); - done(); - }); - }); - - it('should return the error code 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; + const meterProvider = new MeterProvider({ + readers: [ + new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + }), + ], }); + meterProvider.getMeter('test-meter').createCounter('test-counter').add(1); - collectorExporter.export(metrics, result => { - try { - assert.strictEqual(result.code, ExportResultCode.FAILED); - // @ts-expect-error verify error code - assert.strictEqual(result.error.code, 400); - done(); - } catch (e) { - done(e); - } - }); + // act + meterProvider.forceFlush(); + meterProvider.shutdown(); }); }); }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts deleted file mode 100644 index effc732b370..00000000000 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/metricsHelper.ts +++ /dev/null @@ -1,242 +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, - ObservableGauge, - HrTime, -} from '@opentelemetry/api'; -import { Resource } from '@opentelemetry/resources'; -import * as assert from 'assert'; -import { - ExplicitBucketHistogramAggregation, - MeterProvider, - MetricReader, - View, -} from '@opentelemetry/sdk-metrics'; -import { - encodeAsString, - IExportMetricsServiceRequest, - IKeyValue, - IMetric, -} from '@opentelemetry/otlp-transformer'; -import { Stream } from 'stream'; - -export class TestMetricReader extends MetricReader { - protected onForceFlush(): Promise { - return Promise.resolve(undefined); - } - - protected onShutdown(): Promise { - return Promise.resolve(undefined); - } -} - -const testResource = new Resource({ - service: 'ui', - version: 1, - cost: 112.12, -}); - -let reader = new TestMetricReader(); -let meterProvider = new MeterProvider({ - resource: testResource, - readers: [reader], -}); - -let meter = meterProvider.getMeter('default', '0.0.1'); - -export async function collect() { - return (await reader.collect())!; -} - -export function setUp() { - reader = new TestMetricReader(); - meterProvider = new MeterProvider({ - resource: testResource, - views: [ - new View({ - aggregation: new ExplicitBucketHistogramAggregation([0, 100]), - instrumentName: 'int-histogram', - }), - ], - 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 -): ObservableGauge { - const name = 'double-observable-gauge'; - const observableGauge = meter.createObservableGauge(name, { - description: 'sample observable gauge description', - valueType: ValueType.DOUBLE, - }); - observableGauge.addCallback(callback); - - return observableGauge; -} - -export function mockHistogram(): Histogram { - const name = 'int-histogram'; - - return meter.createHistogram(name, { - description: 'sample histogram description', - valueType: ValueType.INT, - }); -} - -export function ensureProtoAttributesAreCorrect(attributes: IKeyValue[]) { - assert.deepStrictEqual( - attributes, - [ - { - key: 'component', - value: { - stringValue: 'document-load', - }, - }, - ], - 'attributes are incorrect' - ); -} - -export function ensureExportedCounterIsCorrect( - 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, - 'AGGREGATION_TEMPORALITY_CUMULATIVE' - ); - - const [dp] = metric.sum.dataPoints; - assert.strictEqual(dp.asInt, '1'); - assert.deepStrictEqual(dp.startTimeUnixNano, encodeAsString(startTime)); - assert.deepStrictEqual(dp.timeUnixNano, encodeAsString(time)); -} - -export function ensureExportedObservableGaugeIsCorrect( - metric: IMetric, - time: HrTime, - startTime: HrTime -) { - assert.strictEqual(metric.name, 'double-observable-gauge'); - 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.strictEqual(dp.asDouble, 6); - assert.deepStrictEqual(dp.startTimeUnixNano, encodeAsString(startTime)); - assert.deepStrictEqual(dp.timeUnixNano, encodeAsString(time)); -} - -export function ensureExportedHistogramIsCorrect( - metric: IMetric, - time: HrTime, - startTime: HrTime, - explicitBounds: number[] = [Infinity], - bucketCounts: string[] = ['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, - 'AGGREGATION_TEMPORALITY_CUMULATIVE' - ); - - const [dp] = metric.histogram.dataPoints; - - assert.strictEqual(dp.sum, 21); - assert.strictEqual(dp.count, '2'); - assert.strictEqual(dp.min, 7); - assert.strictEqual(dp.max, 14); - assert.deepStrictEqual(dp.explicitBounds, explicitBounds); - assert.deepStrictEqual(dp.bucketCounts, bucketCounts); - 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 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; - } -}