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

Track request body size in XHR and Fetch instrumentations #4706

Merged
merged 41 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e349fa4
reduce overlap between (variable shadowing) between individual tests
MustafaHaddara May 10, 2024
656cbc4
update xhr test to check attr names directly instead of using key order
MustafaHaddara May 9, 2024
538e712
update fetch test to read attributes directly instead of using key order
MustafaHaddara May 14, 2024
eaf9786
override api request in beforeEach call
MustafaHaddara May 14, 2024
d6149ca
Add getXHRBodyLength and getFetchBodyLength functions to opentelemetr…
MustafaHaddara May 14, 2024
d97b02b
track request body content length in xhr instrumentation
MustafaHaddara May 14, 2024
860557e
track request body content length in fetch instrumentation
MustafaHaddara May 14, 2024
9dfe663
add changelog entry
MustafaHaddara May 14, 2024
a14c1f9
update browser; webworker tests
MustafaHaddara May 14, 2024
499c8ca
account for older platforms where ReadableStream can't tee()
MustafaHaddara May 22, 2024
19890b5
lint fix: add comment after @ts-expect-error
MustafaHaddara May 30, 2024
bee76c8
make response body measurement opt-in
MustafaHaddara Jul 15, 2024
854cc53
update named exports
MustafaHaddara Aug 12, 2024
24e3b46
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MustafaHaddara Aug 19, 2024
12c42e3
switch to experimental new semantic attribute
MustafaHaddara Aug 19, 2024
91637aa
warn instead of error
MustafaHaddara Aug 19, 2024
08ab734
correctly read string length
MustafaHaddara Aug 19, 2024
8d3c533
consistent return value when we can't calculate length
MustafaHaddara Aug 19, 2024
bf92869
lint fix
MustafaHaddara Aug 19, 2024
f21eb54
update ReadableStream length calculation
MustafaHaddara Aug 23, 2024
102d128
switch to pipeThrough() to reduce memory use
MustafaHaddara Aug 23, 2024
a0a26ea
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MustafaHaddara Aug 23, 2024
8b94dbc
document the semconv attributes we include
MustafaHaddara Aug 23, 2024
3569096
more correct handling of FormData
MustafaHaddara Aug 23, 2024
d011829
support formData.entries()
MustafaHaddara Aug 26, 2024
178e73b
update stream in fetch tests
MustafaHaddara Aug 26, 2024
74cb0ea
add a warning for old platforms
MustafaHaddara Aug 26, 2024
7c2bfe2
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MustafaHaddara Aug 26, 2024
51cc125
fix fetch tests
MustafaHaddara Aug 26, 2024
f0dcca9
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MustafaHaddara Aug 27, 2024
192f440
Merge branch 'main' into request-body-size
JamieDanielson Aug 27, 2024
0007def
Merge branch 'main' into request-body-size
MustafaHaddara Oct 16, 2024
40d01bb
use semconv 1.7 attribute
MustafaHaddara Oct 25, 2024
8192b86
update changelog
MustafaHaddara Oct 25, 2024
f73fccd
Merge branch 'main' into request-body-size
MustafaHaddara Oct 25, 2024
8b7a76d
update changelog
MustafaHaddara Oct 25, 2024
93957de
lint markdown
MustafaHaddara Oct 28, 2024
1c26261
use the correct attribute -.-
MustafaHaddara Oct 29, 2024
8d6705f
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MustafaHaddara Nov 5, 2024
ed6f37f
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MustafaHaddara Nov 12, 2024
5d86c75
duplicate helper utils so they aren't public in sdk-trace-web
MustafaHaddara Nov 14, 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 @@ -47,6 +47,7 @@ All notable changes to experimental packages in this project will be documented
* feat(sdk-node, sdk-logs): add `mergeResourceWithDefaults` flag, which allows opting-out of resources getting merged with the default resource [#4617](https://github.com/open-telemetry/opentelemetry-js/pull/4617)
* default: `true`
* note: `false` will become the default behavior in a future iteration in order to comply with [specification requirements](https://github.com/open-telemetry/opentelemetry-specification/blob/f3511a5ccda376dfd1de76dfa086fc9b35b54757/specification/resource/sdk.md?plain=1#L31-L36)
* feat(instrumentation): Track request body size in XHR and Fetch instrumentations [#4706](https://github.com/open-telemetry/opentelemetry-js/pull/4706) @mustafahaddara

### :bug: (Bug Fix)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ Fetch instrumentation plugin has few options available to choose from. You can s

| Options | Type | Description |
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|-----------------------------------------------------------------------------------------|
| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L64) | `HttpCustomAttributeFunction` | Function for adding custom attributes |
| [`ignoreNetworkEvents`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L67) | `boolean` | Disable network events being added as span events (network events are added by default) |
| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L75) | `HttpCustomAttributeFunction` | Function for adding custom attributes |
| [`ignoreNetworkEvents`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L77) | `boolean` | Disable network events being added as span events (network events are added by default) |
| [`measureRequestSize`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L79) | `boolean` | Measure outgoing request length (outgoing request length is not measured by default) |
JamieDanielson marked this conversation as resolved.
Show resolved Hide resolved

## Semantic Conventions

Expand All @@ -80,12 +81,13 @@ Attributes collected:

| Attribute | Short Description |
| ------------------------------------------- | ------------------------------------------------------------------------------ |
| `http.status_code` | HTTP response status code |
| `http.status_code` | HTTP response status code |
| `http.host` | The value of the HTTP host header |
| `http.user_agent` | Value of the HTTP User-Agent header sent by the client |
| `http.scheme` | The URI scheme identifying the used protocol |
| `http.url` | Full HTTP request URL |
| `http.method` | HTTP request method |
| `http.request_content_length_uncompressed` | Uncompressed size of the request body, if any body exists |

## Useful links

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import {
SEMATTRS_HTTP_SCHEME,
SEMATTRS_HTTP_URL,
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
} from '@opentelemetry/semantic-conventions';
import { FetchError, FetchResponse, SpanData } from './types';
import { getFetchBodyLength } from './utils';
import { VERSION } from './version';
import { _globalThis } from '@opentelemetry/core';

Expand Down Expand Up @@ -74,6 +76,8 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig {
applyCustomAttributesOnSpan?: FetchCustomAttributeFunction;
// Ignore adding network events as span events
ignoreNetworkEvents?: boolean;
/** Measure outgoing request size */
measureRequestSize?: boolean;
}

/**
Expand Down Expand Up @@ -320,6 +324,21 @@ export class FetchInstrumentation extends InstrumentationBase<FetchInstrumentati
}
const spanData = plugin._prepareSpanData(url);

if (plugin.getConfig().measureRequestSize) {
getFetchBodyLength(...args)
.then(length => {
if (!length) return;

createdSpan.setAttribute(
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
length
);
})
.catch(error => {
plugin._diag.warn('getFetchBodyLength', error);
});
}

function endSpanOnError(span: api.Span, error: FetchError) {
plugin._applyAttributesAfterFetch(span, options, error);
plugin._endSpan(span, spanData, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Much of the logic here overlaps with the same utils file in opentelemetry-instrumentation-xml-http-request
// These may be unified in the future.

import * as api from '@opentelemetry/api';

const DIAG_LOGGER = api.diag.createComponentLogger({
namespace: '@opentelemetry/opentelemetry-instrumentation-fetch/utils',
});

/**
* Helper function to determine payload content length for fetch requests
*
* The fetch API is kinda messy: there are a couple of ways the body can be passed in.
*
* In all cases, the body param can be some variation of ReadableStream,
* and ReadableStreams can only be read once! We want to avoid consuming the body here,
* because that would mean that the body never gets sent with the actual fetch request.
*
* Either the first arg is a Request object, which can be cloned
* so we can clone that object and read the body of the clone
* without disturbing the original argument
* However, reading the body here can only be done async; the body() method returns a promise
* this means this entire function has to return a promise
*
* OR the first arg is a url/string
* in which case the second arg has type RequestInit
* RequestInit is NOT cloneable, but RequestInit.body is writable
* so we can chain it into ReadableStream.pipeThrough()
*
* ReadableStream.pipeThrough() lets us process a stream and returns a new stream
* So we can measure the body length as it passes through the pie, but need to attach
* the new stream to the original request
* so that the browser still has access to the body.
*
* @param body
* @returns promise that resolves to the content length of the body
*/
export function getFetchBodyLength(...args: Parameters<typeof fetch>) {
if (args[0] instanceof URL || typeof args[0] === 'string') {
const requestInit = args[1];
if (!requestInit?.body) {
return Promise.resolve();
}
if (requestInit.body instanceof ReadableStream) {
const { body, length } = _getBodyNonDestructively(requestInit.body);
requestInit.body = body;

return length;
} else {
return Promise.resolve(getXHRBodyLength(requestInit.body));
}
} else {
const info = args[0];
if (!info?.body) {
return Promise.resolve();
}

return info
.clone()
.text()
.then(t => getByteLength(t));
}
}

function _getBodyNonDestructively(body: ReadableStream) {
// can't read a ReadableStream without destroying it
// but we CAN pipe it through and return a new ReadableStream

// some (older) platforms don't expose the pipeThrough method and in that scenario, we're out of luck;
// there's no way to read the stream without consuming it.
if (!body.pipeThrough) {
DIAG_LOGGER.warn('Platform has ReadableStream but not pipeThrough!');
return {
body,
length: Promise.resolve(undefined),
};
}

let length = 0;
let resolveLength: (l: number) => void;
const lengthPromise = new Promise<number>(resolve => {
resolveLength = resolve;
});

const transform = new TransformStream({
start() {},
async transform(chunk, controller) {
const bytearray = (await chunk) as Uint8Array;
length += bytearray.byteLength;

controller.enqueue(chunk);
},
flush() {
resolveLength(length);
},
});

return {
body: body.pipeThrough(transform),
length: lengthPromise,
};
}

/**
* Helper function to determine payload content length for XHR requests
* @param body
* @returns content length
*/
export function getXHRBodyLength(
body: Document | XMLHttpRequestBodyInit
): number | undefined {
if (typeof Document !== 'undefined' && body instanceof Document) {
return new XMLSerializer().serializeToString(document).length;
}
// XMLHttpRequestBodyInit expands to the following:
if (body instanceof Blob) {
return body.size;
}

// ArrayBuffer | ArrayBufferView
if ((body as any).byteLength !== undefined) {
return (body as any).byteLength as number;
}

if (body instanceof FormData) {
return getFormDataSize(body);
}

if (body instanceof URLSearchParams) {
return getByteLength(body.toString());
}

if (typeof body === 'string') {
return getByteLength(body);
}

DIAG_LOGGER.warn('unknown body type');
return undefined;
}

const TEXT_ENCODER = new TextEncoder();
function getByteLength(s: string): number {
return TEXT_ENCODER.encode(s).byteLength;
}

function getFormDataSize(formData: FormData): number {
let size = 0;
for (const [key, value] of formData.entries()) {
size += key.length;
if (value instanceof Blob) {
size += value.size;
} else {
size += value.length;
}
}
return size;
}
Loading
Loading