@opentelemetry/instrumentation-fetch
Version:
OpenTelemetry instrumentation for fetch http client in web browsers
214 lines • 7.06 kB
JavaScript
/*
* 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';
import { getStringListFromEnv } from '@opentelemetry/core';
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) {
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) {
// 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;
const lengthPromise = new Promise(resolve => {
resolveLength = resolve;
});
const transform = new TransformStream({
start() { },
async transform(chunk, controller) {
const bytearray = (await chunk);
length += bytearray.byteLength;
controller.enqueue(chunk);
},
flush() {
resolveLength(length);
},
});
return {
body: body.pipeThrough(transform),
length: lengthPromise,
};
}
function isDocument(value) {
return typeof Document !== 'undefined' && value instanceof Document;
}
/**
* Helper function to determine payload content length for XHR requests
* @param body
* @returns content length
*/
export function getXHRBodyLength(body) {
if (isDocument(body)) {
return new XMLSerializer().serializeToString(document).length;
}
// XMLHttpRequestBodyInit expands to the following:
if (typeof body === 'string') {
return getByteLength(body);
}
if (body instanceof Blob) {
return body.size;
}
if (body instanceof FormData) {
return getFormDataSize(body);
}
if (body instanceof URLSearchParams) {
return getByteLength(body.toString());
}
// ArrayBuffer | ArrayBufferView
if (body.byteLength !== undefined) {
return body.byteLength;
}
DIAG_LOGGER.warn('unknown body type');
return undefined;
}
const TEXT_ENCODER = new TextEncoder();
function getByteLength(s) {
return TEXT_ENCODER.encode(s).byteLength;
}
function getFormDataSize(formData) {
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;
}
/**
* Normalize an HTTP request method string per `http.request.method` spec
* https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client-span
*/
export function normalizeHttpRequestMethod(method) {
const knownMethods = getKnownMethods();
const methUpper = method.toUpperCase();
if (methUpper in knownMethods) {
return methUpper;
}
else {
return '_OTHER';
}
}
const DEFAULT_KNOWN_METHODS = {
CONNECT: true,
DELETE: true,
GET: true,
HEAD: true,
OPTIONS: true,
PATCH: true,
POST: true,
PUT: true,
TRACE: true,
};
let knownMethods;
function getKnownMethods() {
if (knownMethods === undefined) {
const cfgMethods = getStringListFromEnv('OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS');
if (cfgMethods && cfgMethods.length > 0) {
knownMethods = {};
cfgMethods.forEach(m => {
knownMethods[m] = true;
});
}
else {
knownMethods = DEFAULT_KNOWN_METHODS;
}
}
return knownMethods;
}
const HTTP_PORT_FROM_PROTOCOL = {
'https:': '443',
'http:': '80',
};
export function serverPortFromUrl(url) {
const serverPort = Number(url.port || HTTP_PORT_FROM_PROTOCOL[url.protocol]);
// Guard with `if (serverPort)` because `Number('') === 0`.
if (serverPort && !isNaN(serverPort)) {
return serverPort;
}
else {
return undefined;
}
}
//# sourceMappingURL=utils.js.map