Skip to content

Commit

Permalink
feat(otlp-exporter): add user-agent header to otlp exporters and re…
Browse files Browse the repository at this point in the history
…move `sendBeacon` which does not support custom headers
  • Loading branch information
llc1123 committed May 17, 2023
1 parent deacef3 commit 8111f47
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<OTLPExporterConfigBase, ExportItem, ServiceRequest> {
protected _headers: Record<string, string>;
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 {
Expand All @@ -73,24 +76,14 @@ export abstract class OTLPExporterBrowserBase<
const body = JSON.stringify(serviceRequest);

const promise = new Promise<void>((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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions experimental/packages/otlp-exporter-base/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@

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;
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
Expand Down
86 changes: 0 additions & 86 deletions experimental/packages/otlp-exporter-base/test/browser/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,83 +39,13 @@ describe('util - browser', () => {
});

describe('when XMLHTTPRequest is used', () => {
let expectedHeaders: Record<string, string>;
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<string, string>;
beforeEach(() => {
Expand All @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions experimental/packages/otlp-grpc-exporter-base/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExportItem, ServiceRequest>(
collector: OTLPGRPCExporterNodeBase<ExportItem, ServiceRequest>,
config: OTLPGRPCExporterConfigNode
Expand Down

0 comments on commit 8111f47

Please sign in to comment.