@erickluis00/otelviewer
Version:
Shared OpenTelemetry tracing utilities, types, and batch processor for Realtime OpenTelemetry Viewer [WIP]
254 lines • 11.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FetchInstrumentation = void 0;
exports.createFetchInstrumentation = createFetchInstrumentation;
const api_1 = require("@opentelemetry/api");
/**
* FetchInstrumentation class that implements the Instrumentation interface
* Patches global fetch and creates spans for tracing
*/
class FetchInstrumentation {
constructor(config) {
this.instrumentationName = '@erickluis00/otelviewer-fetch';
this.instrumentationVersion = '1.0.0';
this._enabled = false;
this._originalFetch = null;
this._config = { enabled: true };
this._tracerProvider = null;
this._meterProvider = null;
this._loggerProvider = null;
this._config = { enabled: true, ...config };
if (this._config.enabled !== false) {
this.enable();
}
}
enable() {
if (this._enabled) {
return;
}
if (!globalThis.fetch) {
console.warn('fetch is not available in this environment');
return;
}
this._originalFetch = globalThis.fetch;
this._enabled = true;
const tracer = api_1.trace.getTracer('fetch-instrumentation', this.instrumentationVersion);
const originalFetch = this._originalFetch;
globalThis.fetch = async function patchedFetch(...args) {
// Extract request info
const [input, init] = args;
const url = typeof input === 'string'
? input
: input instanceof URL
? input.href
: input.url;
const method = init?.method || (input instanceof Request ? input.method : 'GET');
// Start a span in the current context (maintains parent-child relationships)
return tracer.startActiveSpan(`${method}`, {
kind: api_1.SpanKind.CLIENT,
attributes: {
'http.method': method,
'http.url': url,
'middleware.type': 'fetch-instrumentation'
}
}, async (span) => {
try {
// Parse URL to extract path and query
let parsedPath = url;
const query = {};
try {
const urlObj = new URL(url);
parsedPath = urlObj.pathname;
urlObj.searchParams.forEach((value, key) => {
query[key] = value;
});
}
catch {
// If URL parsing fails, try to extract path manually
const pathMatch = url.split('?')[0];
parsedPath = pathMatch || '/';
}
span.setAttribute('http.path', parsedPath);
// Capture request headers
const requestHeaders = {};
if (init?.headers) {
const headers = init.headers instanceof Headers
? init.headers
: new Headers(init.headers);
headers.forEach((value, key) => {
requestHeaders[key] = value;
});
}
else if (input instanceof Request) {
input.headers.forEach((value, key) => {
requestHeaders[key] = value;
});
}
// Capture request body if present
let requestBody;
if (init?.body) {
try {
if (typeof init.body === 'string') {
const contentType = requestHeaders['content-type'] || requestHeaders['Content-Type'] || '';
if (contentType.includes('application/json')) {
requestBody = JSON.parse(init.body);
}
else {
requestBody = init.body;
}
}
else {
// For other types (FormData, Blob, etc.), we can't easily capture
requestBody = '[Non-string body]';
}
}
catch {
requestBody = init.body;
}
}
else if (input instanceof Request && input.body) {
// Request object with body - try to clone and read
try {
const clonedRequest = input.clone();
const bodyText = await clonedRequest.text();
const contentType = requestHeaders['content-type'] || requestHeaders['Content-Type'] || '';
if (contentType.includes('application/json')) {
requestBody = JSON.parse(bodyText);
}
else {
requestBody = bodyText;
}
}
catch {
requestBody = '[Unable to read request body]';
}
}
// Build request data matching HTTP instrumentation pattern
const requestData = {
method,
url,
path: parsedPath,
headers: requestHeaders,
query,
timestamp: new Date().toISOString()
};
if (requestBody !== undefined) {
requestData.body = requestBody;
}
span.setAttribute('input', JSON.stringify(requestData));
// Make the actual fetch call
const response = await originalFetch(...args);
// Capture response headers first
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
// Check content-type header
const contentType = responseHeaders['content-type'] || responseHeaders['Content-Type'] || '';
const isReadableContentType = contentType.includes('application/json') ||
contentType.includes('text/');
// Parse response body
let parsedBody = '[Empty response]';
if (isReadableContentType) {
// Only read JSON/text content types (content-encoding is handled automatically by browser)
try {
// Clone to read body without consuming the original
const clonedResponse = response.clone();
const bodyText = await clonedResponse.text();
// Try to parse as JSON
try {
parsedBody = JSON.parse(bodyText);
}
catch {
// Not JSON, keep as text
parsedBody = bodyText;
}
}
catch {
parsedBody = '[Unable to read response body]';
}
}
else {
// Non-readable content type (binary, image, etc.)
parsedBody = `[Non-readable content type${contentType ? `: ${contentType}` : ''}]`;
}
// Build response data matching HTTP instrumentation pattern
const responseData = {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
body: parsedBody,
timestamp: new Date().toISOString()
};
span.setAttribute('output', JSON.stringify(responseData));
span.setAttribute('http.status_code', response.status);
span.setAttribute('http.status_text', response.statusText);
// Set span status based on HTTP status
if (response.status >= 400) {
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: `HTTP ${response.status}`
});
}
else {
span.setStatus({ code: api_1.SpanStatusCode.OK });
}
span.end();
return response;
}
catch (error) {
span.recordException(error);
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: error.message
});
span.end();
throw error;
}
});
};
}
disable() {
if (!this._enabled || !this._originalFetch) {
return;
}
globalThis.fetch = this._originalFetch;
this._originalFetch = null;
this._enabled = false;
}
setTracerProvider(tracerProvider) {
this._tracerProvider = tracerProvider;
}
setMeterProvider(meterProvider) {
this._meterProvider = meterProvider;
}
setLoggerProvider(loggerProvider) {
this._loggerProvider = loggerProvider;
}
setConfig(config) {
this._config = { ...this._config, ...config };
if (config.enabled === false && this._enabled) {
this.disable();
}
else if (config.enabled !== false && !this._enabled) {
this.enable();
}
}
getConfig() {
return { ...this._config };
}
}
exports.FetchInstrumentation = FetchInstrumentation;
/**
* Creates a fetch instrumentation that patches global fetch and creates spans
* Much simpler than using UndiciInstrumentation - just intercepts fetch directly
*
* This maintains proper span hierarchy and context propagation automatically
* when using tracer.startActiveSpan()
*
* Returns an Instrumentation instance that can be used in Instrumentation[] arrays
*/
function createFetchInstrumentation(config) {
return new FetchInstrumentation(config);
}
//# sourceMappingURL=fetch-instrumentation.js.map