From 8111f478cd63f319b82050e2607686e80538ae06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B2=92=E7=B2=92=E6=A9=99?= Date: Wed, 17 May 2023 16:44:51 +0800 Subject: [PATCH] feat(otlp-exporter): add `user-agent` header to otlp exporters and remove `sendBeacon` which does not support custom headers --- .../browser/OTLPExporterBrowserBase.ts | 61 ++++++------- .../src/platform/browser/util.ts | 47 ++-------- .../src/platform/node/OTLPExporterNodeBase.ts | 18 ++-- .../packages/otlp-exporter-base/src/util.ts | 5 ++ .../test/browser/util.test.ts | 86 ------------------- .../src/OTLPGRPCExporterNodeBase.ts | 10 ++- .../otlp-grpc-exporter-base/src/util.ts | 3 + 7 files changed, 66 insertions(+), 164 deletions(-) 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..16c90ec0d09 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,42 @@ 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 = {}; + + const headersBeforeUserAgent = Object.assign( + { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + baggageUtils.parseKeyPairsIntoRecord(getEnv().OTEL_EXPORTER_OTLP_HEADERS), + parseHeaders(config.headers) + ); + if ( + Object.keys(headersBeforeUserAgent) + .map(key => key.toLowerCase()) + .includes('user-agent') + ) { + diag.warn('User-Agent header should not be set via config.'); } + this._headers = Object.assign(headersBeforeUserAgent, USER_AGENT); } onInit(): void { @@ -73,24 +76,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..e4b61f3401b 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,19 @@ export abstract class OTLPExporterNodeBase< if ((config as any).metadata) { diag.warn('Metadata cannot be set when using http'); } - this.headers = Object.assign( + const headersBeforeUserAgent = Object.assign( this.DEFAULT_HEADERS, - parseHeaders(config.headers), - baggageUtils.parseKeyPairsIntoRecord(getEnv().OTEL_EXPORTER_OTLP_HEADERS) + baggageUtils.parseKeyPairsIntoRecord(getEnv().OTEL_EXPORTER_OTLP_HEADERS), + parseHeaders(config.headers) ); + if ( + Object.keys(headersBeforeUserAgent) + .map(key => key.toLowerCase()) + .includes('user-agent') + ) { + diag.warn('User-Agent header should not be set via 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..1fd226a7603 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 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..87964e56320 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, @@ -58,6 +62,10 @@ export abstract class OTLPGRPCExporterNodeBase< for (const [k, v] of Object.entries(headers)) { 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