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 fetch sender for ServiceWorker environment #3542

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f654e84
feat(otlp-exporter-base): Add fetch sender for ServiceWorker environm…
sugi Jan 17, 2023
d5951df
feat(otlp-exporter-base): fix send method detect condition and more t…
sugi Jan 20, 2023
a0aa0c9
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Jan 20, 2023
7a36740
fix(otlp-exporter-base): improve fetch error handling.
sugi Jan 28, 2023
7f66982
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Jan 28, 2023
5a714bf
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Mar 6, 2023
e5f0454
feat(otlp-exporter-base): implement retry on fetch sender to compat w…
sugi Mar 6, 2023
8a4311e
chore: lint style fix.
sugi Mar 6, 2023
6927154
chore: fix test description.
sugi May 7, 2023
a613a5b
chore: fix test description.
sugi May 7, 2023
11cb4a3
chore: pin package version.
sugi May 7, 2023
af91d48
chore: pin package version.
sugi May 7, 2023
1dda106
chore: fix typo
sugi May 7, 2023
c52538e
chore: merge imports.
sugi May 7, 2023
b89476b
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi May 7, 2023
2b5db3e
Merge branch 'main' into service-worker-support
pichlermarc May 9, 2023
7dd9675
Merge branch 'main' into service-worker-support
pichlermarc May 11, 2023
8187bd0
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Jun 4, 2023
75e8ed7
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Oct 16, 2023
975f9f9
fix: stop to check response body and check return code;
sugi Oct 19, 2023
da0c607
chore: use Enum
sugi Oct 19, 2023
f39ea11
Merge branch 'main' into service-worker-support
sugi Oct 19, 2023
fbba203
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Nov 4, 2023
f409773
chore: add diag message for test.
sugi Nov 4, 2023
16909ad
chore: import current package-lock.json (adding fetch-mock)
sugi Nov 4, 2023
8bda0e1
fix: Move entry to unreleased.
sugi Nov 4, 2023
72db4f2
chore: update submodules to match with upstream/main.
sugi Nov 4, 2023
14975a8
chore: fix by lint.
sugi Nov 14, 2023
2b22dc7
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Nov 14, 2023
5359519
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Feb 3, 2024
6918bbc
Merge remote-tracking branch 'upstream/main' into service-worker-support
sugi Feb 11, 2024
45a8f76
chore: Add fetch mode test for zipkinExporter.
sugi Feb 11, 2024
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 @@ -16,6 +16,7 @@ All notable changes to experimental packages in this project will be documented
* feat(sdk-node): install diag logger with OTEL_LOG_LEVEL [#3627](https://github.com/open-telemetry/opentelemetry-js/pull/3627) @legendecas
* feat(otlp-exporter-base): add retries [#3207](https://github.com/open-telemetry/opentelemetry-js/pull/3207) @svetlanabrennan
* feat(sdk-node): override IdGenerator when using NodeSDK [#3645](https://github.com/open-telemetry/opentelemetry-js/pull/3645) @haddasbronfman
* feat(otlp-exporter-base): Add fetch sender for WebWorker and ServiceWorker environment. [#3542](https://github.com/open-telemetry/opentelemetry-js/pull/3542) @sugi

### :bug: (Bug Fix)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"babel-loader": "8.2.3",
"codecov": "3.8.3",
"cpx": "1.5.0",
"fetch-mock": "^9.11.0",
sugi marked this conversation as resolved.
Show resolved Hide resolved
"istanbul-instrumenter-loader": "3.0.1",
"karma": "6.3.16",
"karma-chrome-launcher": "3.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
OTLPExporterError,
} from '@opentelemetry/otlp-exporter-base';
import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer';
import { FetchMockStatic, MockCall } from 'fetch-mock/esm/client';
const fetchMock = require('fetch-mock/esm/client').default as FetchMockStatic;
MSNev marked this conversation as resolved.
Show resolved Hide resolved

describe('OTLPTraceExporter - web', () => {
let collectorTraceExporter: OTLPTraceExporter;
Expand Down Expand Up @@ -270,6 +272,140 @@ describe('OTLPTraceExporter - web', () => {
});
});
});

describe('when both "sendBeacon" and "XMLHTTPRequest" are NOT available (service worker env)', () => {
let clock: sinon.SinonFakeTimers;
let stubXhr: sinon.SinonStub;
const url = 'http://foo.bar.com';
beforeEach(() => {
clock = sinon.useFakeTimers();

stubBeacon.value(undefined);
stubXhr = sinon.stub(globalThis, 'XMLHttpRequest').value(undefined);
fetchMock.mock(url, 200);
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
});
afterEach(() => {
sinon.restore();
clock.restore();
fetchMock.restore();
});

it('should successfully send the spans using fetch', done => {
collectorTraceExporter.export(spans, () => {
try {
assert.ok(fetchMock.called(url));
const request = fetchMock.lastCall(url)?.[1];
assert.ok(request);
assert.strictEqual(request.method, 'POST');

const body = request.body?.toString();
assert.ok(body);
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(stubBeacon.callCount, 0);
assert.strictEqual(stubXhr.callCount, 0);
ensureExportTraceServiceRequestIsSet(json);
done();
} catch (e) {
done(e);
}
});
});

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, () => {
try {
assert.strictEqual(fetchMock.calls('*').length, 1);
assert.ok(fetchMock.called(url));

assert.strictEqual(
spyLoggerDebug.args[spyLoggerDebug.callCount - 1][0],
'Request Success'
);
assert.strictEqual(spyLoggerError.args.length, 0);
assert.strictEqual(stubBeacon.callCount, 0);
assert.strictEqual(stubXhr.callCount, 0);

done();
} catch (e) {
done(e);
}
});
});

it('should log the error message', done => {
fetchMock.restore();
fetchMock.mock(url, 400);
collectorTraceExporter.export(spans, result => {
try {
assert.ok(fetchMock.called(url));
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
assert.ok(result.error?.message.includes('Failed to export'));
assert.strictEqual(stubBeacon.callCount, 0);
assert.strictEqual(stubXhr.callCount, 0);
done();
} catch (e) {
done(e);
}
});
});

it('should send custom headers', done => {
collectorTraceExporter = new OTLPTraceExporter(
Object.assign({}, collectorExporterConfig, {
headers: {
'My-Custom-Header1': '123',
'My-Custom-Header2': 'abc',
},
})
);

collectorTraceExporter.export(spans, () => {
try {
assert.ok(fetchMock.called(url));
const request = fetchMock.lastCall(url)?.[1];
assert.ok(request);
assert.ok(request.headers);
const sentHeadersObj = Object.fromEntries(
Object.entries(request.headers)
);
assert.strictEqual(sentHeadersObj['My-Custom-Header1'], '123');
assert.strictEqual(sentHeadersObj['My-Custom-Header2'], 'abc');

assert.strictEqual(stubBeacon.callCount, 0);
assert.strictEqual(stubXhr.callCount, 0);
done();
} catch (e) {
done(e);
}
});
});
});
});

describe('export - common', () => {
Expand Down Expand Up @@ -432,6 +568,66 @@ describe('OTLPTraceExporter - web', () => {
});
});
});

describe('when both "sendBeacon" and "XMLHttpRequest" are NOT available', () => {
let clock: sinon.SinonFakeTimers;
let stubXhr: sinon.SinonStub;
beforeEach(() => {
clock = sinon.useFakeTimers();

stubBeacon.value(undefined);
stubXhr = sinon.stub(globalThis, 'XMLHttpRequest').value(undefined);
fetchMock.mock('*', 200);
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
});
afterEach(() => {
sinon.restore();
clock.restore();
fetchMock.restore();
});

const assertRequesetHeaders = (
pichlermarc marked this conversation as resolved.
Show resolved Hide resolved
call: MockCall | undefined,
expected: Record<string, string>
) => {
assert.ok(call);
const headers = call[1]?.headers;
assert.ok(headers, 'invalid header');
ensureHeadersContain(
Object.fromEntries(Object.entries(headers)),
expected
);
};

it('should successfully send spans using XMLHttpRequest', done => {
sugi marked this conversation as resolved.
Show resolved Hide resolved
collectorTraceExporter.export(spans, () => {
try {
assert.ok(fetchMock.called('*'));
assertRequesetHeaders(fetchMock.lastCall('*'), customHeaders);
assert.strictEqual(stubBeacon.callCount, 0);
assert.strictEqual(stubOpen.callCount, 0);
assert.strictEqual(stubXhr.callCount, 0);
done();
} catch (e) {
done(e);
}
});
});
it('should log the timeout request error message', done => {
collectorTraceExporter.export(spans, result => {
try {
assert.strictEqual(result.code, core.ExportResultCode.FAILED);
const error = result.error as OTLPExporterError;
assert.ok(error !== undefined);
assert.strictEqual(error.message, 'Request Timeout');
done();
} catch (e) {
done(e);
}
});
clock.tick(10000);
});
});
});

describe('export - concurrency limit', () => {
Expand Down Expand Up @@ -701,4 +897,108 @@ describe('export with retry - real http request destroyed', () => {
});
}).timeout(3000);
});

describe('when both "sendBeacon" and "XMLHttpRequest" are NOT available', () => {
const url = 'http://localhost:4318/v1/traces';
let clock: sinon.SinonFakeTimers | undefined;
beforeEach(() => {
(window.navigator as any).sendBeacon = false;
sinon.stub(globalThis, 'XMLHttpRequest').value(undefined);
collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig);
});
afterEach(() => {
clock?.restore();
fetchMock.reset();
});
it('should log the timeout request error message when retrying with exponential backoff with jitter', done => {
spans = [];
spans.push(Object.assign({}, mockedReadableSpan));
clock = sinon.useFakeTimers();

let tries = 0;
fetchMock.mock(url, () => {
tries++;
return 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(tries, 1);
done();
});
clock.tick(2000);
}).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));
clock = sinon.useFakeTimers();

let retry = 0;
fetchMock.mock(url, () => {
retry++;
return { status: 503, headers: { '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();
});
clock.tick(2000);
}).timeout(3000);

it('should log the timeout request error message when retry-after header is a date', async () => {
spans = [];
spans.push(Object.assign({}, mockedReadableSpan));

let tries = 0;
fetchMock.mock(url, () => {
tries++;
const d = new Date();
d.setSeconds(d.getSeconds() + 1);
return { status: 503, headers: { 'Retry-After': d.toUTCString() } };
});

await new Promise(r => setTimeout(r, 1000 - (Date.now() % 1000))); // wait to start export exactly in seconds
return new Promise(resolve => {
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(tries, 2);
resolve();
});
});
}).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 tries = 0;
fetchMock.mock(url, () => {
tries++;
const d = new Date();
d.setSeconds(d.getSeconds() + 120);
return { status: 503, headers: { 'Retry-After': d.toUTCString() } };
});

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(tries, 1);
done();
});
}).timeout(3000);
});
});
Loading