diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 7bdbe404cba..065e375e006 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -17,11 +17,13 @@ All notable changes to experimental packages in this project will be documented * feat(exporter-logs-otlp-http): otlp-http exporter for logs. [#3764](https://github.com/open-telemetry/opentelemetry-js/pull/3764/) @fuaiyi * feat(otlp-trace-exporters): Add User-Agent header to OTLP trace exporters. [#3790](https://github.com/open-telemetry/opentelemetry-js/pull/3790) @JamieDanielson * feat(otlp-metric-exporters): Add User-Agent header to OTLP metric exporters. [#3806](https://github.com/open-telemetry/opentelemetry-js/pull/3806) @JamieDanielson +* feat(otlp-exporters): Move User-Agent header to OTLP exporter base packages. [#3811](https://github.com/open-telemetry/opentelemetry-js/pull/3811) @llc1123 ### :bug: (Bug Fix) * fix(sdk-node): use resource interface instead of concrete class [#3803](https://github.com/open-telemetry/opentelemetry-js/pull/3803) @blumamir * fix(sdk-logs): remove includeTraceContext configuration and use LogRecord context when available [#3817](https://github.com/open-telemetry/opentelemetry-js/pull/3817) @hectorhdzg +* fix(otlp-exporters): correct applying order of http headers/gRPC metadata [#3811](https://github.com/open-telemetry/opentelemetry-js/pull/3811) @llc1123 ### :books: (Refine Doc) diff --git a/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts b/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts index 675ce83db0a..176c37ca4b4 100644 --- a/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts +++ b/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts @@ -37,14 +37,22 @@ export class OTLPLogExporter implements LogRecordExporter { constructor(config: OTLPGRPCExporterConfigNode = {}) { - super(config); const headers = baggageUtils.parseKeyPairsIntoRecord( getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS ); - this.metadata ||= new Metadata(); + const metadata = new Metadata(); for (const [k, v] of Object.entries(headers)) { - this.metadata.set(k, v); + metadata.set(k, v); } + if (config.metadata) { + for (const [k, v] of Object.entries(config.metadata.getMap())) { + metadata.set(k, v); + } + } + super({ + ...config, + metadata, + }); } convert(logRecords: ReadableLogRecord[]): IExportLogsServiceRequest { diff --git a/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts index c229d5e5ddd..1c8cf96943f 100644 --- a/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts @@ -37,6 +37,7 @@ import { IExportLogsServiceRequest, IResourceLogs, } from '@opentelemetry/otlp-transformer'; +import { VERSION } from '../src/version'; const logsServiceProtoPath = 'opentelemetry/proto/collector/logs/v1/logs_service.proto'; @@ -326,16 +327,19 @@ describe('when configuring via environment', () => { assert.deepStrictEqual(collectorExporter.metadata?.get('foo'), ['bar']); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should override global headers config with signal headers defined via env', () => { + it('should override global headers config with signal headers defined via env but not config from parameters', () => { const metadata = new grpc.Metadata(); - metadata.set('foo', 'bar'); - metadata.set('goo', 'lol'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=jar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=boo'; + metadata.set('foo', 'jar'); + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = + 'foo=boo,bar=loo,User-Agent=baz'; const collectorExporter = new OTLPLogExporter({ metadata }); - assert.deepStrictEqual(collectorExporter.metadata?.get('foo'), ['boo']); - assert.deepStrictEqual(collectorExporter.metadata?.get('bar'), ['foo']); - assert.deepStrictEqual(collectorExporter.metadata?.get('goo'), ['lol']); + assert.deepStrictEqual(collectorExporter.metadata?.get('foo'), ['jar']); + assert.deepStrictEqual(collectorExporter.metadata?.get('bar'), ['loo']); + assert.deepStrictEqual(collectorExporter.metadata?.get('goo'), ['loo']); + assert.deepStrictEqual(collectorExporter.metadata?.get('User-Agent'), [ + `OTel-OTLP-Exporter-JavaScript/${VERSION}`, + ]); envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); diff --git a/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts index c99826a176f..36cce9a40d1 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts @@ -28,11 +28,6 @@ import { createExportTraceServiceRequest, IExportTraceServiceRequest, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from './version'; - -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; /** * OTLP Trace Exporter for Node @@ -42,17 +37,22 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPGRPCExporterConfigNode = {}) { - super(config); - const headers = { - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ), - }; - this.metadata ||= new Metadata(); + const headers = baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS + ); + const metadata = new Metadata(); for (const [k, v] of Object.entries(headers)) { - this.metadata.set(k, v); + metadata.set(k, v); + } + if (config.metadata) { + for (const [k, v] of Object.entries(config.metadata.getMap())) { + metadata.set(k, v); + } } + super({ + ...config, + metadata, + }); } convert(spans: ReadableSpan[]): IExportTraceServiceRequest { diff --git a/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts index 17e55110105..e3f036b248f 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts @@ -27,7 +27,6 @@ import * as grpc from '@grpc/grpc-js'; import * as path from 'path'; import * as sinon from 'sinon'; import { OTLPTraceExporter } from '../src'; -import { VERSION } from '../src/version'; import { ensureExportedSpanIsCorrect, @@ -42,6 +41,7 @@ import { IExportTraceServiceRequest, IResourceSpans, } from '@opentelemetry/otlp-transformer'; +import { VERSION } from '../src/version'; const traceServiceProtoPath = 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; @@ -337,22 +337,19 @@ describe('when configuring via environment', () => { assert.deepStrictEqual(collectorExporter.metadata?.get('foo'), ['bar']); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should include user agent in header', () => { - const collectorExporter = new OTLPTraceExporter(); + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + const metadata = new grpc.Metadata(); + metadata.set('foo', 'jar'); + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = + 'foo=boo,bar=loo,User-Agent=baz'; + const collectorExporter = new OTLPTraceExporter({ metadata }); + assert.deepStrictEqual(collectorExporter.metadata?.get('foo'), ['jar']); + assert.deepStrictEqual(collectorExporter.metadata?.get('bar'), ['loo']); + assert.deepStrictEqual(collectorExporter.metadata?.get('goo'), ['loo']); assert.deepStrictEqual(collectorExporter.metadata?.get('User-Agent'), [ `OTel-OTLP-Exporter-JavaScript/${VERSION}`, ]); - }); - it('should override global headers config with signal headers defined via env', () => { - const metadata = new grpc.Metadata(); - metadata.set('foo', 'bar'); - metadata.set('goo', 'lol'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=jar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPTraceExporter({ metadata }); - assert.deepStrictEqual(collectorExporter.metadata?.get('foo'), ['boo']); - assert.deepStrictEqual(collectorExporter.metadata?.get('bar'), ['foo']); - assert.deepStrictEqual(collectorExporter.metadata?.get('goo'), ['lol']); envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); diff --git a/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts index 8a343306c4a..a1fc5336bb9 100644 --- a/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts @@ -38,13 +38,15 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterConfigBase = {}) { - super(config); - this._headers = Object.assign( - this._headers, - baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ) - ); + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS + ), + ...config.headers, + }, + }); } convert(spans: ReadableSpan[]): IExportTraceServiceRequest { return createExportTraceServiceRequest(spans, true); diff --git a/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts index f10fbd0ec0a..691e39e5267 100644 --- a/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts @@ -26,13 +26,9 @@ import { createExportTraceServiceRequest, IExportTraceServiceRequest, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../../version'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/traces'; const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; /** * Collector Trace Exporter for Node @@ -42,14 +38,15 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterNodeConfigBase = {}) { - super(config); - this.headers = { - ...this.headers, - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ), - }; + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS + ), + ...config.headers, + }, + }); } convert(spans: ReadableSpan[]): IExportTraceServiceRequest { 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 index 4e8bc1d6e17..863da40eeac 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -33,6 +33,7 @@ import { OTLPExporterError, } from '@opentelemetry/otlp-exporter-base'; import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; +import { VERSION } from '../../src/version'; describe('OTLPTraceExporter - web', () => { let collectorTraceExporter: OTLPTraceExporter; @@ -85,189 +86,104 @@ describe('OTLPTraceExporter - web', () => { }); describe('export', () => { + let server: any; + let clock: sinon.SinonFakeTimers; beforeEach(() => { collectorExporterConfig = { hostname: 'foo', 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); - } - }); - }); + clock = sinon.useFakeTimers(); + collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); + server = sinon.fakeServer.create(); + }); - 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, - }; + afterEach(() => { + server.restore(); + }); - diag.setLogger(diagLogger, DiagLogLevel.ALL); + it('should successfully send the spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); - stubBeacon.returns(true); + queueMicrotask(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); - collectorTraceExporter.export(spans, () => {}); + const body = request.requestBody; + const json = JSON.parse(body) as IExportTraceServiceRequest; + const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - setTimeout(() => { - const response: any = spyLoggerDebug.args[2][0]; - assert.strictEqual(response, 'sendBeacon - can send'); - assert.strictEqual(spyLoggerError.args.length, 0); + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + ensureSpanIsCorrect(span1); - done(); - }); - }); + const resource = json.resourceSpans?.[0].resource; + assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); + ensureWebResourceIsCorrect(resource); - it('should log the error message', done => { - stubBeacon.returns(false); + assert.strictEqual(stubBeacon.callCount, 0); + ensureExportTraceServiceRequestIsSet(json); - collectorTraceExporter.export(spans, result => { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok(result.error?.message.includes('cannot send')); - done(); - }); + clock.restore(); + done(); }); }); - 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; - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - server = sinon.fakeServer.create(); - }); - afterEach(() => { - server.restore(); - }); - - it('should successfully send the spans using XMLHttpRequest', done => { - collectorTraceExporter.export(spans, () => {}); - - queueMicrotask(() => { - const request = server.requests[0]; - assert.strictEqual(request.method, 'POST'); - assert.strictEqual(request.url, 'http://foo.bar.com'); - - const body = request.requestBody; - const json = JSON.parse(body) as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; + 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, + }; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureSpanIsCorrect(span1); + diag.setLogger(diagLogger, DiagLogLevel.ALL); - const resource = json.resourceSpans?.[0].resource; - assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); - ensureWebResourceIsCorrect(resource); + collectorTraceExporter.export(spans, () => {}); - assert.strictEqual(stubBeacon.callCount, 0); - ensureExportTraceServiceRequestIsSet(json); + queueMicrotask(() => { + const request = server.requests[0]; + request.respond(200); + 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(); - }); + clock.restore(); + done(); }); + }); - 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); - 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(); - }); + it('should log the error message', done => { + collectorTraceExporter.export(spans, result => { + assert.deepStrictEqual(result.code, ExportResultCode.FAILED); + assert.ok(result.error?.message.includes('Failed to export')); + done(); }); - it('should log the error message', done => { - collectorTraceExporter.export(spans, result => { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok(result.error?.message.includes('Failed to export')); - done(); - }); - - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(400); - clock.restore(); - done(); - }); + queueMicrotask(() => { + const request = server.requests[0]; + request.respond(400); + clock.restore(); + done(); }); + }); - it('should send custom headers', done => { - collectorTraceExporter.export(spans, () => {}); + it('should send custom headers', done => { + collectorTraceExporter.export(spans, () => {}); - queueMicrotask(() => { - const request = server.requests[0]; - request.respond(200); + queueMicrotask(() => { + const request = server.requests[0]; + request.respond(200); - assert.strictEqual(stubBeacon.callCount, 0); - clock.restore(); - done(); - }); + assert.strictEqual(stubBeacon.callCount, 0); + clock.restore(); + done(); }); }); }); @@ -349,6 +265,7 @@ describe('OTLPTraceExporter - web', () => { describe('export with custom headers', () => { let server: any; + let clock: sinon.SinonFakeTimers; const customHeaders = { foo: 'bar', bar: 'baz', @@ -359,77 +276,44 @@ describe('OTLPTraceExporter - web', () => { headers: customHeaders, }; server = sinon.fakeServer.create(); + // 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); }); 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, () => {}); + it('should successfully send spans using XMLHttpRequest', done => { + collectorTraceExporter.export(spans, () => {}); - queueMicrotask(() => { - const [{ requestHeaders }] = server.requests; + queueMicrotask(() => { + const [{ requestHeaders }] = server.requests; - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); + ensureHeadersContain(requestHeaders, customHeaders); + assert.strictEqual(stubBeacon.callCount, 0); + assert.strictEqual(stubOpen.callCount, 0); - clock.restore(); - done(); - }); + clock.restore(); + done(); }); }); + it('should log the timeout request error message', done => { + const responseSpy = sinon.spy(); + collectorTraceExporter.export(spans, responseSpy); + clock.tick(10000); + clock.restore(); - 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(() => { - const [{ requestHeaders }] = server.requests; - - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); - - clock.restore(); - done(); - }); - }); - it('should log the timeout request error message', done => { - const responseSpy = sinon.spy(); - collectorTraceExporter.export(spans, responseSpy); - clock.tick(10000); - clock.restore(); - - 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.message, 'Request Timeout'); + 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.message, 'Request Timeout'); - done(); - }); + done(); }); }); }); @@ -570,14 +454,24 @@ describe('when configuring via environment', () => { assert.strictEqual(collectorExporter._headers.foo, 'bar'); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPTraceExporter({ headers: {} }); + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = + 'foo=boo,bar=loo,user-agent=baz'; + const collectorExporter = new OTLPTraceExporter({ + headers: { foo: 'jar' }, + }); + // @ts-expect-error access internal property for testing + assert.strictEqual(collectorExporter._headers.foo, 'jar'); // @ts-expect-error access internal property for testing - assert.strictEqual(collectorExporter._headers.foo, 'boo'); + assert.strictEqual(collectorExporter._headers.bar, 'loo'); // @ts-expect-error access internal property for testing - assert.strictEqual(collectorExporter._headers.bar, 'foo'); + assert.strictEqual(collectorExporter._headers.goo, 'loo'); + assert.strictEqual( + // @ts-expect-error access internal property for testing + collectorExporter._headers['user-agent'], + `OTel-OTLP-Exporter-JavaScript/${VERSION}` + ); envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); @@ -596,109 +490,92 @@ describe('export with retry - real http request destroyed', () => { collectorExporterConfig = { timeoutMillis: 1500, }; + collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); }); afterEach(() => { server.restore(); }); - describe('when "sendBeacon" is NOT available', () => { - beforeEach(() => { - (window.navigator as any).sendBeacon = false; - collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); - }); - it('should log the timeout request error message when retrying with exponential backoff with jitter', done => { - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); + it('should log the timeout request error message when retrying with exponential backoff with jitter', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); - let retry = 0; - server.respondWith( - 'http://localhost:4318/v1/traces', - function (xhr: any) { - retry++; - xhr.respond(503); - } - ); + let retry = 0; + server.respondWith('http://localhost:4318/v1/traces', function (xhr: any) { + retry++; + xhr.respond(503); + }); - collectorTraceExporter.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'); - assert.strictEqual(retry, 1); - done(); - }); - }).timeout(3000); + collectorTraceExporter.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'); + assert.strictEqual(retry, 1); + done(); + }); + }).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)); + 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 retry = 0; - server.respondWith( - 'http://localhost:4318/v1/traces', - function (xhr: any) { - retry++; - xhr.respond(503, { 'Retry-After': 3 }); - } - ); + let retry = 0; + server.respondWith('http://localhost:4318/v1/traces', function (xhr: any) { + retry++; + xhr.respond(503, { 'Retry-After': 3 }); + }); - collectorTraceExporter.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'); - assert.strictEqual(retry, 1); - done(); - }); - }).timeout(3000); - it('should log the timeout request error message when retry-after header is a date', done => { - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); + collectorTraceExporter.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'); + assert.strictEqual(retry, 1); + done(); + }); + }).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() + 1); - xhr.respond(503, { 'Retry-After': d }); - } - ); + let retry = 0; + server.respondWith('http://localhost:4318/v1/traces', function (xhr: any) { + retry++; + const d = new Date(); + d.setSeconds(d.getSeconds() + 1); + xhr.respond(503, { 'Retry-After': d }); + }); - collectorTraceExporter.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'); - assert.strictEqual(retry, 2); - done(); - }); - }).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)); + collectorTraceExporter.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'); + assert.strictEqual(retry, 2); + done(); + }); + }).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 }); - } - ); + 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 => { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - assert.strictEqual(retry, 1); - done(); - }); - }).timeout(3000); - }); + collectorTraceExporter.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'); + assert.strictEqual(retry, 1); + done(); + }); + }).timeout(3000); }); 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 index 9238260d309..3f6679db87b 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts @@ -161,19 +161,20 @@ describe('OTLPTraceExporter - node with json over http', () => { assert.strictEqual(collectorExporter.headers.foo, 'bar'); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should include user agent in header', () => { - const collectorExporter = new OTLPTraceExporter(); + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = + 'foo=boo,bar=loo,user-agent=baz'; + const collectorExporter = new OTLPTraceExporter({ + headers: { foo: 'jar' }, + }); + assert.strictEqual(collectorExporter.headers.foo, 'jar'); + assert.strictEqual(collectorExporter.headers.bar, 'loo'); + assert.strictEqual(collectorExporter.headers.goo, 'loo'); assert.strictEqual( - collectorExporter.headers['User-Agent'], + collectorExporter.headers['user-agent'], `OTel-OTLP-Exporter-JavaScript/${VERSION}` ); - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual(collectorExporter.headers.foo, 'boo'); - assert.strictEqual(collectorExporter.headers.bar, 'foo'); envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); diff --git a/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts index 890268333fa..b2a233a7c76 100644 --- a/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts @@ -41,14 +41,17 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterConfigBase = {}) { - super(config); - this._headers = Object.assign( - this._headers, - baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ) - ); + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS + ), + ...config.headers, + }, + }); } + convert(spans: ReadableSpan[]): IExportTraceServiceRequest { return createExportTraceServiceRequest(spans); } diff --git a/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts index 210a16145a5..962d9b0b53c 100644 --- a/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts @@ -29,13 +29,9 @@ import { createExportTraceServiceRequest, IExportTraceServiceRequest, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../../version'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/traces'; const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; /** * Collector Trace Exporter for Node with protobuf @@ -45,14 +41,15 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterNodeConfigBase = {}) { - super(config); - this.headers = { - ...this.headers, - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ), - }; + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS + ), + ...config.headers, + }, + }); } convert(spans: ReadableSpan[]): IExportTraceServiceRequest { 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 c0a604ce90b..0213e0986d7 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 @@ -53,16 +53,6 @@ describe('OTLPTraceExporter - node with proto over http', () => { sinon.restore(); }); - describe('default behavior for headers', () => { - const collectorExporter = new OTLPTraceExporter(); - it('should include user agent in header', () => { - assert.strictEqual( - collectorExporter.headers['User-Agent'], - `OTel-OTLP-Exporter-JavaScript/${VERSION}` - ); - }); - }); - describe('when configuring via environment', () => { const envSource = process.env; it('should use url defined in env that ends with root path and append version and signal path', () => { @@ -146,12 +136,20 @@ describe('OTLPTraceExporter - node with proto over http', () => { assert.strictEqual(collectorExporter.headers.foo, 'bar'); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual(collectorExporter.headers.foo, 'boo'); - assert.strictEqual(collectorExporter.headers.bar, 'foo'); + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = + 'foo=boo,bar=loo,user-agent=baz'; + const collectorExporter = new OTLPTraceExporter({ + headers: { foo: 'jar' }, + }); + assert.strictEqual(collectorExporter.headers.foo, 'jar'); + assert.strictEqual(collectorExporter.headers.bar, 'loo'); + assert.strictEqual(collectorExporter.headers.goo, 'loo'); + assert.strictEqual( + collectorExporter.headers['user-agent'], + `OTel-OTLP-Exporter-JavaScript/${VERSION}` + ); envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts index 0ada75d4375..2146764e95f 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts @@ -32,29 +32,28 @@ import { createExportMetricsServiceRequest, IExportMetricsServiceRequest, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from './version'; - -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; class OTLPMetricExporterProxy extends OTLPGRPCExporterNodeBase< ResourceMetrics, IExportMetricsServiceRequest > { constructor(config?: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions) { - super(config); - const headers = { - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ), - }; - - this.metadata ||= new Metadata(); + const headers = baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS + ); + const metadata = new Metadata(); for (const [k, v] of Object.entries(headers)) { - this.metadata.set(k, v); + metadata.set(k, v); + } + if (config?.metadata) { + for (const [k, v] of Object.entries(config.metadata.getMap())) { + metadata.set(k, v); + } } + super({ + ...config, + metadata, + }); } getServiceProtoPath(): string { diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts index 9e4e27ecb0a..25b26ef39ef 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts @@ -315,34 +315,31 @@ describe('when configuring via environment', () => { ); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should include user agent in header', () => { - const collectorExporter = new OTLPMetricExporter(); - assert.deepStrictEqual( - collectorExporter._otlpExporter.metadata?.get('User-Agent'), - [`OTel-OTLP-Exporter-JavaScript/${VERSION}`] - ); - }); - it('should override global headers config with signal headers defined via env', () => { + it('should override global headers config with signal headers defined via env but not config from parameters', () => { const metadata = new grpc.Metadata(); - metadata.set('foo', 'bar'); - metadata.set('goo', 'lol'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=jar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; + metadata.set('foo', 'jar'); + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'foo=boo,bar=loo,User-Agent=baz'; const collectorExporter = new OTLPMetricExporter({ metadata, temporalityPreference: AggregationTemporality.CUMULATIVE, }); assert.deepStrictEqual( collectorExporter._otlpExporter.metadata?.get('foo'), - ['boo'] + ['jar'] ); assert.deepStrictEqual( collectorExporter._otlpExporter.metadata?.get('bar'), - ['foo'] + ['loo'] ); assert.deepStrictEqual( collectorExporter._otlpExporter.metadata?.get('goo'), - ['lol'] + ['loo'] + ); + assert.deepStrictEqual( + collectorExporter._otlpExporter.metadata?.get('User-Agent'), + [`OTel-OTLP-Exporter-JavaScript/${VERSION}`] ); envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts index 73c99a06938..90e6fe281f7 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts @@ -37,13 +37,15 @@ class OTLPExporterBrowserProxy extends OTLPExporterBrowserBase< IExportMetricsServiceRequest > { constructor(config?: OTLPMetricExporterOptions & OTLPExporterConfigBase) { - super(config); - this._headers = Object.assign( - this._headers, - baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ) - ); + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS + ), + ...config?.headers, + }, + }); } getDefaultUrl(config: OTLPExporterConfigBase): string { diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts index f83e414e701..867f4fef8a6 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts @@ -28,27 +28,24 @@ import { createExportMetricsServiceRequest, IExportMetricsServiceRequest, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../../version'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/metrics'; const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; class OTLPExporterNodeProxy extends OTLPExporterNodeBase< ResourceMetrics, IExportMetricsServiceRequest > { constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { - super(config); - this.headers = { - ...this.headers, - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ), - }; + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS + ), + ...config?.headers, + }, + }); } convert(metrics: ResourceMetrics[]): IExportMetricsServiceRequest { 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 index 7dbc7cc889c..2a0b10c4301 100644 --- 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 @@ -47,11 +47,11 @@ import { import { OTLPMetricExporterOptions } from '../../src'; import { OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; +import { VERSION } from '../../src/version'; 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; @@ -60,7 +60,6 @@ describe('OTLPMetricExporter - web', () => { 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 => { @@ -98,277 +97,140 @@ describe('OTLPMetricExporter - web', () => { }); describe('export', () => { - describe('when "sendBeacon" is available', () => { - beforeEach(() => { - collectorExporter = new OTLPMetricExporter({ - url: 'http://foo.bar.com', - temporalityPreference: AggregationTemporality.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, - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .endTime - ), - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .startTime - ) - ); - - assert.ok( - typeof metric2 !== 'undefined', - "second metric doesn't exist" - ); - ensureObservableGaugeIsCorrect( - metric2, - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .endTime - ), - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .startTime - ), - 6, - 'double-observable-gauge2' - ); - - assert.ok( - typeof metric3 !== 'undefined', - "third metric doesn't exist" - ); - ensureHistogramIsCorrect( - metric3, - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .endTime - ), - hrTimeToNanoseconds( - 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, () => {}); - - setTimeout(() => { - const response: any = debugStub.args[2][0]; - assert.strictEqual(response, 'sendBeacon - can send'); - assert.strictEqual(errorStub.args.length, 0); - - done(); - }); + let server: any; + beforeEach(() => { + collectorExporter = new OTLPMetricExporter({ + url: 'http://foo.bar.com', + temporalityPreference: AggregationTemporality.CUMULATIVE, }); - - it('should log the error message', done => { - stubBeacon.returns(false); - - collectorExporter.export(metrics, result => { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok(result.error?.message.includes('cannot send')); - done(); - }); + // Overwrites the start time to make tests consistent + Object.defineProperty(collectorExporter, '_startTime', { + value: 1592602232694000000, }); + server = sinon.fakeServer.create(); + }); + afterEach(() => { + server.restore(); }); - 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: AggregationTemporality.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, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse(body) as 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, + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0].endTime + ), + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] + .startTime + ) + ); + + assert.ok( + typeof metric2 !== 'undefined', + "second metric doesn't exist" + ); + ensureObservableGaugeIsCorrect( + metric2, + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] + .endTime + ), + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] + .startTime + ), + 6, + 'double-observable-gauge2' + ); + + assert.ok(typeof metric3 !== 'undefined', "third metric doesn't exist"); + ensureHistogramIsCorrect( + metric3, + hrTimeToNanoseconds( + metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] + .endTime + ), + hrTimeToNanoseconds( + 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); + + ensureExportMetricsServiceRequestIsSet(json); + + done(); }); + }); - it('should successfully send the metrics using XMLHttpRequest', done => { - collectorExporter.export(metrics, () => {}); - - setTimeout(() => { - const request = server.requests[0]; - assert.strictEqual(request.method, 'POST'); - assert.strictEqual(request.url, 'http://foo.bar.com'); - - const body = request.requestBody; - const json = JSON.parse(body) as 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, - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .endTime - ), - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[counterIndex].dataPoints[0] - .startTime - ) - ); - - assert.ok( - typeof metric2 !== 'undefined', - "second metric doesn't exist" - ); - ensureObservableGaugeIsCorrect( - metric2, - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .endTime - ), - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[observableIndex].dataPoints[0] - .startTime - ), - 6, - 'double-observable-gauge2' - ); - - assert.ok( - typeof metric3 !== 'undefined', - "third metric doesn't exist" - ); - ensureHistogramIsCorrect( - metric3, - hrTimeToNanoseconds( - metrics.scopeMetrics[0].metrics[histogramIndex].dataPoints[0] - .endTime - ), - hrTimeToNanoseconds( - 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(); - }); - }); + it('should log the successful message', done => { + collectorExporter.export(metrics, () => {}); - it('should log the successful message', done => { - collectorExporter.export(metrics, () => {}); + setTimeout(() => { + const request = server.requests[0]; + request.respond(200); - setTimeout(() => { - 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); - const response: any = debugStub.args[2][0]; - assert.strictEqual(response, 'xhr success'); - assert.strictEqual(errorStub.args.length, 0); + done(); + }); + }); - assert.strictEqual(stubBeacon.callCount, 0); - done(); - }); + it('should log the error message', done => { + collectorExporter.export(metrics, result => { + assert.deepStrictEqual(result.code, ExportResultCode.FAILED); + assert.ok(result.error?.message.includes('Failed to export')); + done(); }); - it('should log the error message', done => { - collectorExporter.export(metrics, result => { - assert.deepStrictEqual(result.code, ExportResultCode.FAILED); - assert.ok(result.error?.message.includes('Failed to export')); - assert.strictEqual(stubBeacon.callCount, 0); - done(); - }); - - setTimeout(() => { - const request = server.requests[0]; - request.respond(400); - }); + setTimeout(() => { + const request = server.requests[0]; + request.respond(400); }); - it('should send custom headers', done => { - collectorExporter.export(metrics, () => {}); + }); + it('should send custom headers', done => { + collectorExporter.export(metrics, () => {}); - setTimeout(() => { - const request = server.requests[0]; - request.respond(200); + setTimeout(() => { + const request = server.requests[0]; + request.respond(200); - assert.strictEqual(stubBeacon.callCount, 0); - done(); - }); + done(); }); }); }); @@ -389,49 +251,23 @@ describe('OTLPMetricExporter - web', () => { temporalityPreference: AggregationTemporality.CUMULATIVE, }; server = sinon.fakeServer.create(); + collectorExporter = new OTLPMetricExporter(collectorExporterConfig); }); 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, () => {}); - it('should successfully send metrics using XMLHttpRequest', done => { - collectorExporter.export(metrics, () => {}); + setTimeout(() => { + const [{ requestHeaders }] = server.requests; - setTimeout(() => { - const [{ requestHeaders }] = server.requests; + ensureHeadersContain(requestHeaders, customHeaders); + assert.strictEqual(stubOpen.callCount, 0); - ensureHeadersContain(requestHeaders, customHeaders); - assert.strictEqual(stubBeacon.callCount, 0); - assert.strictEqual(stubOpen.callCount, 0); - - done(); - }); + done(); }); }); }); @@ -526,20 +362,29 @@ describe('when configuring via environment', () => { ); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'foo=boo,bar=loo,user-agent=baz'; const collectorExporter = new OTLPMetricExporter({ - headers: {}, + headers: { foo: 'jar' }, temporalityPreference: AggregationTemporality.CUMULATIVE, }); assert.strictEqual( collectorExporter['_otlpExporter']['_headers'].foo, - 'boo' + 'jar' ); assert.strictEqual( collectorExporter['_otlpExporter']['_headers'].bar, - 'foo' + 'loo' + ); + assert.strictEqual( + collectorExporter['_otlpExporter']['_headers'].goo, + 'loo' + ); + assert.strictEqual( + collectorExporter['_otlpExporter']['_headers']['user-agent'], + `OTel-OTLP-Exporter-JavaScript/${VERSION}` ); envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; 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 index 042e5ebb4be..573cf3a8370 100644 --- 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 @@ -189,19 +189,20 @@ describe('OTLPMetricExporter - node with json over http', () => { assert.strictEqual(collectorExporter._otlpExporter.headers.foo, 'bar'); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should include user agent in header', () => { - const collectorExporter = new OTLPMetricExporter(); + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'foo=boo,bar=loo,user-agent=baz'; + const collectorExporter = new OTLPMetricExporter({ + headers: { foo: 'jar' }, + }); + assert.strictEqual(collectorExporter._otlpExporter.headers.foo, 'jar'); + assert.strictEqual(collectorExporter._otlpExporter.headers.bar, 'loo'); + assert.strictEqual(collectorExporter._otlpExporter.headers.goo, 'loo'); assert.strictEqual( - collectorExporter._otlpExporter.headers['User-Agent'], + collectorExporter._otlpExporter.headers['user-agent'], `OTel-OTLP-Exporter-JavaScript/${VERSION}` ); - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual(collectorExporter._otlpExporter.headers.foo, 'boo'); - assert.strictEqual(collectorExporter._otlpExporter.headers.bar, 'foo'); envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts index 8d1fb114b3f..c6919cb3c86 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts @@ -31,27 +31,24 @@ import { createExportMetricsServiceRequest, IExportMetricsServiceRequest, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from './version'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/metrics'; const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; class OTLPMetricExporterNodeProxy extends OTLPProtoExporterNodeBase< ResourceMetrics, IExportMetricsServiceRequest > { constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { - super(config); - this.headers = { - ...this.headers, - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ), - }; + super({ + ...config, + headers: { + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS + ), + ...config?.headers, + }, + }); } convert(metrics: ResourceMetrics[]): IExportMetricsServiceRequest { 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 d3270b6a9f3..8a3f76b8844 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 @@ -61,16 +61,6 @@ describe('OTLPMetricExporter - node with proto over http', () => { sinon.restore(); }); - describe('default behavior for headers', () => { - const collectorExporter = new OTLPMetricExporter(); - it('should include user agent in header', () => { - assert.strictEqual( - collectorExporter._otlpExporter.headers['User-Agent'], - `OTel-OTLP-Exporter-JavaScript/${VERSION}` - ); - }); - }); - describe('when configuring via environment', () => { const envSource = process.env; it('should use url defined in env that ends with root path and append version and signal path', () => { @@ -155,12 +145,20 @@ describe('OTLPMetricExporter - node with proto over http', () => { assert.strictEqual(collectorExporter._otlpExporter.headers.foo, 'bar'); envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual(collectorExporter._otlpExporter.headers.foo, 'boo'); - assert.strictEqual(collectorExporter._otlpExporter.headers.bar, 'foo'); + it('should override global headers config with signal headers defined via env but not config from parameters', () => { + envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo,goo=loo'; + envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'foo=boo,bar=loo,user-agent=baz'; + const collectorExporter = new OTLPMetricExporter({ + headers: { foo: 'jar' }, + }); + assert.strictEqual(collectorExporter._otlpExporter.headers.foo, 'jar'); + assert.strictEqual(collectorExporter._otlpExporter.headers.bar, 'loo'); + assert.strictEqual(collectorExporter._otlpExporter.headers.goo, 'loo'); + assert.strictEqual( + collectorExporter._otlpExporter.headers['user-agent'], + `OTel-OTLP-Exporter-JavaScript/${VERSION}` + ); envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index 57556d81a58..c7fba21b058 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -17,39 +17,41 @@ import { OTLPExporterBase } from '../../OTLPExporterBase'; import { OTLPExporterConfigBase } from '../../types'; import * as otlpTypes from '../../types'; -import { parseHeaders } from '../../util'; -import { sendWithBeacon, sendWithXhr } from './util'; +import { USER_AGENT, parseHeaders } from '../../util'; +import { sendWithXhr } from './util'; import { diag } from '@opentelemetry/api'; import { getEnv, baggageUtils } from '@opentelemetry/core'; /** - * Collector Metric Exporter abstract base class + * OTLP Exporter abstract base class */ export abstract class OTLPExporterBrowserBase< ExportItem, ServiceRequest > extends OTLPExporterBase { protected _headers: Record; - private _useXHR: boolean = false; /** * @param config */ constructor(config: OTLPExporterConfigBase = {}) { super(config); - this._useXHR = - !!config.headers || typeof navigator.sendBeacon !== 'function'; - if (this._useXHR) { - this._headers = Object.assign( - {}, - parseHeaders(config.headers), - baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_HEADERS - ) - ); - } else { - this._headers = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((config as any).metadata) { + diag.warn('Metadata cannot be set when using http'); + } + const headersBeforeUserAgent = parseHeaders({ + accept: 'application/json', + 'content-type': 'application/json', + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_HEADERS + ), + ...config.headers, + }); + if (Object.keys(headersBeforeUserAgent).includes('user-agent')) { + diag.warn('Header "user-agent" should not be set by config.'); } + this._headers = Object.assign(headersBeforeUserAgent, USER_AGENT); } onInit(): void { @@ -73,24 +75,14 @@ export abstract class OTLPExporterBrowserBase< const body = JSON.stringify(serviceRequest); const promise = new Promise((resolve, reject) => { - if (this._useXHR) { - sendWithXhr( - body, - this.url, - this._headers, - this.timeoutMillis, - resolve, - reject - ); - } else { - sendWithBeacon( - body, - this.url, - { type: 'application/json' }, - resolve, - reject - ); - } + sendWithXhr( + body, + this.url, + this._headers, + this.timeoutMillis, + resolve, + reject + ); }).then(onSuccess, onError); this._sendingPromises.push(promise); diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index fade4afa88b..e2d25deae30 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -24,30 +24,6 @@ import { parseRetryAfterToMills, } from '../../util'; -/** - * Send metrics/spans using browser navigator.sendBeacon - * @param body - * @param url - * @param blobPropertyBag - * @param onSuccess - * @param onError - */ -export function sendWithBeacon( - body: string, - url: string, - blobPropertyBag: BlobPropertyBag, - onSuccess: () => void, - onError: (error: OTLPExporterError) => void -): void { - if (navigator.sendBeacon(url, new Blob([body], blobPropertyBag))) { - diag.debug('sendBeacon - can send', body); - onSuccess(); - } else { - const error = new OTLPExporterError(`sendBeacon - cannot send ${body}`); - onError(error); - } -} - /** * function to send metrics/spans using browser XMLHttpRequest * used when navigator.sendBeacon is not available @@ -88,16 +64,12 @@ export function sendWithXhr( xhr = new XMLHttpRequest(); xhr.open('POST', url); - const defaultHeaders = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - - Object.entries({ - ...defaultHeaders, - ...headers, - }).forEach(([k, v]) => { - xhr.setRequestHeader(k, v); + Object.entries(headers).forEach(([k, v]) => { + try { + xhr.setRequestHeader(k, v); + } catch (e) { + // Chrome will throw an error on setting user-agent, ignore it. + } }); xhr.send(body); @@ -114,10 +86,9 @@ export function sendWithXhr( minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay; // retry after interval specified in Retry-After header - if (xhr.getResponseHeader('Retry-After')) { - retryTime = parseRetryAfterToMills( - xhr.getResponseHeader('Retry-After')! - ); + const retryAfter = xhr.getResponseHeader('Retry-After'); + if (retryAfter) { + retryTime = parseRetryAfterToMills(retryAfter); } else { // exponential backoff with jitter retryTime = Math.round( diff --git a/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts b/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts index 088a0fd0a02..7c5f7c55521 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts @@ -20,13 +20,13 @@ import type * as https from 'https'; import { OTLPExporterBase } from '../../OTLPExporterBase'; import { OTLPExporterNodeConfigBase, CompressionAlgorithm } from './types'; import * as otlpTypes from '../../types'; -import { parseHeaders } from '../../util'; +import { USER_AGENT, parseHeaders } from '../../util'; import { createHttpAgent, sendWithHttp, configureCompression } from './util'; import { diag } from '@opentelemetry/api'; import { getEnv, baggageUtils } from '@opentelemetry/core'; /** - * Collector Metric Exporter abstract base class + * OTLP Exporter abstract base class */ export abstract class OTLPExporterNodeBase< ExportItem, @@ -47,11 +47,17 @@ export abstract class OTLPExporterNodeBase< if ((config as any).metadata) { diag.warn('Metadata cannot be set when using http'); } - this.headers = Object.assign( - this.DEFAULT_HEADERS, - parseHeaders(config.headers), - baggageUtils.parseKeyPairsIntoRecord(getEnv().OTEL_EXPORTER_OTLP_HEADERS) - ); + const headersBeforeUserAgent = parseHeaders({ + ...this.DEFAULT_HEADERS, + ...baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_HEADERS + ), + ...config.headers, + }); + if (Object.keys(headersBeforeUserAgent).includes('user-agent')) { + diag.warn('Header "user-agent" should not be set by config.'); + } + this.headers = Object.assign(headersBeforeUserAgent, USER_AGENT); this.agent = createHttpAgent(config); this.compression = configureCompression(config.compression); } diff --git a/experimental/packages/otlp-exporter-base/src/util.ts b/experimental/packages/otlp-exporter-base/src/util.ts index f5dc70c9e88..f53c423832e 100644 --- a/experimental/packages/otlp-exporter-base/src/util.ts +++ b/experimental/packages/otlp-exporter-base/src/util.ts @@ -16,6 +16,7 @@ import { diag } from '@opentelemetry/api'; import { getEnv } from '@opentelemetry/core'; +import { VERSION } from './version'; const DEFAULT_TRACE_TIMEOUT = 10000; export const DEFAULT_EXPORT_MAX_ATTEMPTS = 5; @@ -23,6 +24,10 @@ export const DEFAULT_EXPORT_INITIAL_BACKOFF = 1000; export const DEFAULT_EXPORT_MAX_BACKOFF = 5000; export const DEFAULT_EXPORT_BACKOFF_MULTIPLIER = 1.5; +export const USER_AGENT = { + 'user-agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, +}; + /** * Parses headers from config leaving only those that have defined values * @param partialHeaders @@ -33,9 +38,11 @@ export function parseHeaders( const headers: Record = {}; Object.entries(partialHeaders).forEach(([key, value]) => { if (typeof value !== 'undefined') { - headers[key] = String(value); + headers[key.toLowerCase()] = String(value); } else { - diag.warn(`Header "${key}" has wrong value and will be ignored`); + diag.warn( + `Header "${key.toLowerCase()}" has wrong value and will be ignored` + ); } }); return headers; diff --git a/experimental/packages/otlp-exporter-base/test/browser/util.test.ts b/experimental/packages/otlp-exporter-base/test/browser/util.test.ts index 1dd3b77d588..081a7b3346e 100644 --- a/experimental/packages/otlp-exporter-base/test/browser/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/browser/util.test.ts @@ -39,83 +39,13 @@ describe('util - browser', () => { }); describe('when XMLHTTPRequest is used', () => { - let expectedHeaders: Record; 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(); - - expectedHeaders = { - // ;charset=utf-8 is applied by sinon.fakeServer - 'Content-Type': 'application/json;charset=utf-8', - Accept: 'application/json', - }; - }); - describe('and Content-Type header is set', () => { - beforeEach(() => { - const explicitContentType = { - 'Content-Type': 'application/json', - }; - const exporterTimeout = 10000; - sendWithXhr( - body, - url, - explicitContentType, - exporterTimeout, - onSuccessStub, - onErrorStub - ); - }); - it('Request Headers should contain "Content-Type" header', done => { - nextTick(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - clock.restore(); - done(); - }); - }); - it('Request Headers should contain "Accept" header', done => { - nextTick(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - clock.restore(); - done(); - }); - }); }); - describe('and empty headers are set', () => { - beforeEach(() => { - const emptyHeaders = {}; - // use default exporter timeout - const exporterTimeout = 10000; - sendWithXhr( - body, - url, - emptyHeaders, - exporterTimeout, - onSuccessStub, - onErrorStub - ); - }); - it('Request Headers should contain "Content-Type" header', done => { - nextTick(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - clock.restore(); - done(); - }); - }); - it('Request Headers should contain "Accept" header', done => { - nextTick(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - clock.restore(); - done(); - }); - }); - }); describe('and custom headers are set', () => { let customHeaders: Record; beforeEach(() => { @@ -130,22 +60,6 @@ describe('util - browser', () => { onErrorStub ); }); - it('Request Headers should contain "Content-Type" header', done => { - nextTick(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - clock.restore(); - done(); - }); - }); - it('Request Headers should contain "Accept" header', done => { - nextTick(() => { - const { requestHeaders } = server.requests[0]; - ensureHeadersContain(requestHeaders, expectedHeaders); - clock.restore(); - done(); - }); - }); it('Request Headers should contain custom headers', done => { nextTick(() => { const { requestHeaders } = server.requests[0]; diff --git a/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts b/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts index 884505daa86..5d4efa37c6b 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts @@ -23,7 +23,11 @@ import { } from './types'; import { ServiceClient } from './types'; import { getEnv, baggageUtils } from '@opentelemetry/core'; -import { configureCompression, GrpcCompressionAlgorithm } from './util'; +import { + configureCompression, + GrpcCompressionAlgorithm, + USER_AGENT, +} from './util'; import { OTLPExporterBase, OTLPExporterError, @@ -54,10 +58,19 @@ export abstract class OTLPGRPCExporterNodeBase< const headers = baggageUtils.parseKeyPairsIntoRecord( getEnv().OTEL_EXPORTER_OTLP_HEADERS ); - this.metadata = config.metadata || new Metadata(); + this.metadata = new Metadata(); for (const [k, v] of Object.entries(headers)) { this.metadata.set(k, v); } + if (config.metadata) { + for (const [k, v] of Object.entries(config.metadata.getMap())) { + this.metadata.set(k, v); + } + } + if (this.metadata.get('User-Agent')) { + diag.warn('User-Agent header should not be set via config.'); + } + this.metadata.set('User-Agent', USER_AGENT); this.compression = configureCompression(config.compression); } diff --git a/experimental/packages/otlp-grpc-exporter-base/src/util.ts b/experimental/packages/otlp-grpc-exporter-base/src/util.ts index 7e1d58dff74..f7a14b44deb 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/util.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/util.ts @@ -35,9 +35,12 @@ import { import { MetricExportServiceClient } from './MetricsExportServiceClient'; import { TraceExportServiceClient } from './TraceExportServiceClient'; import { LogsExportServiceClient } from './LogsExportServiceClient'; +import { VERSION } from './version'; export const DEFAULT_COLLECTOR_URL = 'http://localhost:4317'; +export const USER_AGENT = `OTel-OTLP-Exporter-JavaScript/${VERSION}`; + export function onInit( collector: OTLPGRPCExporterNodeBase, config: OTLPGRPCExporterConfigNode diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts index 4e9f95d5c0d..1d06f8e0a0d 100644 --- a/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts @@ -38,7 +38,14 @@ export abstract class OTLPProtoExporterBrowserBase< ServiceRequest > extends OTLPExporterBaseMain { constructor(config: OTLPExporterConfigBase = {}) { - super(config); + super({ + ...config, + headers: { + accept: 'application/x-protobuf', + 'content-type': 'application/x-protobuf', + ...config.headers, + }, + }); } private _getExportRequestProto( @@ -75,11 +82,7 @@ export abstract class OTLPProtoExporterBrowserBase< sendWithXhr( new Blob([body], { type: 'application/x-protobuf' }), this.url, - { - ...this._headers, - 'Content-Type': 'application/x-protobuf', - Accept: 'application/x-protobuf', - }, + this._headers, this.timeoutMillis, onSuccess, onError