Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(otlp-exporter-base): add retries #3207

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7376ce5
feat(otlp-exporter-base): add retries to sendWithHttp
svetlanabrennan Aug 4, 2022
43320ee
feat(otlp-exporter-base): add tests and update abort logic
svetlanabrennan Aug 19, 2022
21771d5
feat(otlp-exporter-base): fix lint
svetlanabrennan Aug 26, 2022
b5fcd3d
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Aug 30, 2022
c9e54b0
feat(otlp-exporter-base): add retry test
svetlanabrennan Sep 22, 2022
75a50ad
feat(otlp-exporter-base): add retry to browser exporter and add tests
svetlanabrennan Oct 6, 2022
5bb910b
feat(otlp-exporter-base): refactor
svetlanabrennan Oct 6, 2022
bdff2b0
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Oct 6, 2022
213d228
feat(otlp-exporter-base): add jitter
svetlanabrennan Oct 20, 2022
5987db3
feat(otlp-exporter-base): initialize reqIsDestroyed to false
svetlanabrennan Oct 20, 2022
917e5cc
feat(otlp-exporter-base): add throttle logic
svetlanabrennan Oct 21, 2022
7e1d0c8
feat(otlp-exporter-base): add retry to readme
svetlanabrennan Oct 21, 2022
b2deb31
Merge branch 'add-retries-to-exporters' of github.com:svetlanabrennan…
svetlanabrennan Oct 21, 2022
24fa721
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Oct 21, 2022
7e45c91
feat(otlp-exporter-base): add changelog
svetlanabrennan Oct 21, 2022
e327d18
feat(otlp-exporter-base): update throttle time function
svetlanabrennan Oct 24, 2022
e3014b1
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Oct 24, 2022
3855c0f
feat(otlp-exporter-base): refactor sec difference in throttle fun
svetlanabrennan Oct 24, 2022
cf833aa
Merge branch 'add-retries-to-exporters' of github.com:svetlanabrennan…
svetlanabrennan Oct 24, 2022
4a658e7
feat(otlp-exporter-base): fix lint
svetlanabrennan Oct 24, 2022
ee0d214
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Jan 13, 2023
549e6f6
feat(otlp-exporter-base): fix lint
svetlanabrennan Jan 13, 2023
e9b6981
feat(otlp-exporter-base): fix lint
svetlanabrennan Jan 13, 2023
2b05f20
Merge remote-tracking branch 'upstream/main' into add-retries-to-expo…
svetlanabrennan Jan 13, 2023
f6e151f
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Jan 31, 2023
396b556
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Feb 8, 2023
9045027
feat(otlp-exporter-base): refactor retrieve throttle time func
svetlanabrennan Feb 16, 2023
e5547ee
Merge branch 'add-retries-to-exporters' of github.com:svetlanabrennan…
svetlanabrennan Feb 16, 2023
1d561fc
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Feb 16, 2023
ea268a3
feat(otlp-exporter-base): fix lint
svetlanabrennan Feb 16, 2023
f1a2b3d
Merge branch 'add-retries-to-exporters' of github.com:svetlanabrennan…
svetlanabrennan Feb 16, 2023
1074c2a
feat(otlp-exporter-base): move parseRetryAfterToMills to utils file
svetlanabrennan Feb 22, 2023
bab22bf
Merge branch 'main' into add-retries-to-exporters
svetlanabrennan Feb 22, 2023
02572c4
Merge branch 'main' into add-retries-to-exporters
legendecas Feb 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ All notable changes to experimental packages in this project will be documented
* deps: remove unused proto-loader dependencies and update grpc-js and proto-loader versions [#3337](https://github.com/open-telemetry/opentelemetry-js/pull/3337) @seemk
* feat(metrics-exporters): configure temporality via environment variable [#3305](https://github.com/open-telemetry/opentelemetry-js/pull/3305) @pichlermarc
* feat(console-metric-exporter): add temporality configuration [#3387](https://github.com/open-telemetry/opentelemetry-js/pull/3387) @pichlermarc
* feat(otlp-exporter-base): add retries [#3207](https://github.com/open-telemetry/opentelemetry-js/pull/3207) @svetlanabrennan

### :bug: (Bug Fix)

Expand Down
15 changes: 15 additions & 0 deletions experimental/packages/exporter-trace-otlp-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ To override the default timeout duration, use the following options:

> Providing `timeoutMillis` with `collectorOptions` takes precedence and overrides timeout set with environment variables.

## OTLP Exporter Retry

OTLP requires that transient errors be handled with a [retry strategy](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#retry).

This retry policy has the following configuration, which there is currently no way to customize.

+ `DEFAULT_EXPORT_MAX_ATTEMPTS`: The maximum number of attempts, including the original request. Defaults to 5.
+ `DEFAULT_EXPORT_INITIAL_BACKOFF`: The initial backoff duration. Defaults to 1 second.
+ `DEFAULT_EXPORT_MAX_BACKOFF`: The maximum backoff duration. Defaults to 5 seconds.
+ `DEFAULT_EXPORT_BACKOFF_MULTIPLIER`: The backoff multiplier. Defaults to 1.5.

This retry policy first checks if the response has a `'Retry-After'` header. If there is a `'Retry-After'` header, the exporter will wait the amount specified in the `'Retry-After'` header before retrying. If there is no `'Retry-After'` header, the exporter will use an exponential backoff with jitter retry strategy.

> The exporter will retry exporting within the [exporter timeout configuration](#Exporter-Timeout-Configuration) time.

## Running opentelemetry-collector locally to see the traces

1. Go to `examples/otlp-exporter-node`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,123 @@ describe('when configuring via environment', () => {
envSource.OTEL_EXPORTER_OTLP_HEADERS = '';
});
});

describe('export with retry - real http request destroyed', () => {
let server: any;
let collectorTraceExporter: OTLPTraceExporter;
let collectorExporterConfig: OTLPExporterConfigBase;
let spans: ReadableSpan[];

beforeEach(() => {
server = sinon.fakeServer.create({
autoRespond: true,
});
collectorExporterConfig = {
timeoutMillis: 1500,
};
});

afterEach(() => {
server.restore();
});

describe('when "sendBeacon" is NOT available', () => {
beforeEach(() => {
(window.navigator as any).sendBeacon = false;
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
});
it('should log the 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);
}
);

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));

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));

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));

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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -551,38 +551,3 @@ describe('export - real http request destroyed before response received', () =>
}, 0);
});
});

describe('export - real http request destroyed after response received', () => {
let collectorExporter: OTLPTraceExporter;
let collectorExporterConfig: OTLPExporterNodeConfigBase;
let spans: ReadableSpan[];

const server = http.createServer((_, res) => {
res.write('writing something');
});
before(done => {
server.listen(8081, done);
});
after(done => {
server.close(done);
});
it('should log the timeout request error message', done => {
collectorExporterConfig = {
url: 'http://localhost:8081',
timeoutMillis: 300,
};
collectorExporter = new OTLPTraceExporter(collectorExporterConfig);
spans = [];
spans.push(Object.assign({}, mockedReadableSpan));

setTimeout(() => {
collectorExporter.export(spans, result => {
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
const error = result.error as OTLPExporterError;
assert.ok(error !== undefined);
assert.strictEqual(error.message, 'Request Timeout');
done();
});
}, 0);
});
});
15 changes: 15 additions & 0 deletions experimental/packages/exporter-trace-otlp-proto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ To override the default timeout duration, use the following options:

> Providing `timeoutMillis` with `collectorOptions` takes precedence and overrides timeout set with environment variables.

## OTLP Exporter Retry

OTLP requires that transient errors be handled with a [retry strategy](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#retry).

This retry policy has the following configuration, which there is currently no way to customize.

+ `DEFAULT_EXPORT_MAX_ATTEMPTS`: The maximum number of attempts, including the original request. Defaults to 5.
+ `DEFAULT_EXPORT_INITIAL_BACKOFF`: The initial backoff duration. Defaults to 1 second.
+ `DEFAULT_EXPORT_MAX_BACKOFF`: The maximum backoff duration. Defaults to 5 seconds.
+ `DEFAULT_EXPORT_BACKOFF_MULTIPLIER`: The backoff multiplier. Defaults to 1.5.

This retry policy first checks if the response has a `'Retry-After'` header. If there is a `'Retry-After'` header, the exporter will wait the amount specified in the `'Retry-After'` header before retrying. If there is no `'Retry-After'` header, the exporter will use an exponential backoff with jitter retry strategy.

> The exporter will retry exporting within the [exporter timeout configuration](#Exporter-Timeout-Configuration) time.

## Running opentelemetry-collector locally to see the traces

1. Go to examples/otlp-exporter-node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
*/
import { diag } from '@opentelemetry/api';
import { OTLPExporterError } from '../../types';
import {
DEFAULT_EXPORT_MAX_ATTEMPTS,
DEFAULT_EXPORT_INITIAL_BACKOFF,
DEFAULT_EXPORT_BACKOFF_MULTIPLIER,
DEFAULT_EXPORT_MAX_BACKOFF,
isExportRetryable,
parseRetryAfterToMills,
} from '../../util';

/**
* Send metrics/spans using browser navigator.sendBeacon
Expand Down Expand Up @@ -57,47 +65,99 @@ export function sendWithXhr(
onSuccess: () => void,
onError: (error: OTLPExporterError) => void
): void {
let reqIsDestroyed: boolean;
let retryTimer: ReturnType<typeof setTimeout>;
let xhr: XMLHttpRequest;
let reqIsDestroyed = false;

const exporterTimer = setTimeout(() => {
clearTimeout(retryTimer);
reqIsDestroyed = true;
xhr.abort();

if (xhr.readyState === XMLHttpRequest.DONE) {
const err = new OTLPExporterError('Request Timeout');
onError(err);
} else {
xhr.abort();
}
}, exporterTimeout);

const xhr = new XMLHttpRequest();
xhr.open('POST', url);
const sendWithRetry = (
retries = DEFAULT_EXPORT_MAX_ATTEMPTS,
minDelay = DEFAULT_EXPORT_INITIAL_BACKOFF
) => {
xhr = new XMLHttpRequest();
xhr.open('POST', url);

const defaultHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
const defaultHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
};

Object.entries({
...defaultHeaders,
...headers,
}).forEach(([k, v]) => {
xhr.setRequestHeader(k, v);
});
Object.entries({
...defaultHeaders,
...headers,
}).forEach(([k, v]) => {
xhr.setRequestHeader(k, v);
});

xhr.send(body);
xhr.send(body);

xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status <= 299) {
clearTimeout(exporterTimer);
diag.debug('xhr success', body);
onSuccess();
} else if (reqIsDestroyed) {
const error = new OTLPExporterError('Request Timeout', xhr.status);
onError(error);
} else {
const error = new OTLPExporterError(
`Failed to export with XHR (status: ${xhr.status})`,
xhr.status
);
clearTimeout(exporterTimer);
onError(error);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && reqIsDestroyed === false) {
if (xhr.status >= 200 && xhr.status <= 299) {
diag.debug('xhr success', body);
onSuccess();
clearTimeout(exporterTimer);
clearTimeout(retryTimer);
} else if (xhr.status && isExportRetryable(xhr.status) && retries > 0) {
let retryTime: number;
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')!
);
} else {
// exponential backoff with jitter
retryTime = Math.round(
Math.random() * (DEFAULT_EXPORT_MAX_BACKOFF - minDelay) + minDelay
);
}

retryTimer = setTimeout(() => {
sendWithRetry(retries - 1, minDelay);
}, retryTime);
} else {
const error = new OTLPExporterError(
`Failed to export with XHR (status: ${xhr.status})`,
xhr.status
);
onError(error);
clearTimeout(exporterTimer);
clearTimeout(retryTimer);
}
}
}
};

xhr.onabort = () => {
if (reqIsDestroyed) {
const err = new OTLPExporterError('Request Timeout');
onError(err);
}
clearTimeout(exporterTimer);
clearTimeout(retryTimer);
};

xhr.onerror = () => {
if (reqIsDestroyed) {
const err = new OTLPExporterError('Request Timeout');
onError(err);
}
clearTimeout(exporterTimer);
clearTimeout(retryTimer);
};
};

sendWithRetry();
}
Loading