autotel-cloudflare
Version:
The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling
137 lines (120 loc) • 4.12 kB
text/typescript
/**
* Global fetch() instrumentation for autotel-edge
*
* Automatically traces all outgoing fetch() calls with:
* - HTTP method, URL, status code
* - Request/response headers
* - Automatic context propagation
* - Error tracking
*/
import {
trace,
context as api_context,
propagation,
SpanStatusCode,
SpanKind,
} from '@opentelemetry/api';
import { getActiveConfig, WorkerTracer } from 'autotel-edge';
/**
* Gather HTTP request attributes following OpenTelemetry semantic conventions
*/
function gatherRequestAttributes(request: Request): Record<string, any> {
const url = new URL(request.url);
const config = getActiveConfig();
const redactQuery = config?.dataSafety?.redactQueryParams === true;
return {
'http.request.method': request.method.toUpperCase(),
'url.full': redactQuery ? `${url.origin}${url.pathname}` : request.url,
'url.scheme': url.protocol.replace(':', ''),
'server.address': url.host,
'url.path': url.pathname,
'url.query': redactQuery ? (url.search ? '[REDACTED]' : '') : url.search,
'network.protocol.name': 'http',
'user_agent.original': request.headers.get('user-agent') || undefined,
};
}
/**
* Gather HTTP response attributes
*/
function gatherResponseAttributes(response: Response): Record<string, any> {
return {
'http.response.status_code': response.status,
'http.response.body.size': response.headers.get('content-length') || undefined,
};
}
/**
* Instrument the global fetch function
*
* This wraps globalThis.fetch to automatically create spans for all outgoing HTTP requests.
*
* **Note:** This is called automatically when the library is initialized with
* `instrumentation.instrumentGlobalFetch: true` (default).
*/
export function instrumentGlobalFetch(): void {
const originalFetch = globalThis.fetch;
const instrumentedFetch = function fetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
const request = new Request(input, init);
// Skip non-HTTP requests
if (!request.url.startsWith('http')) {
return originalFetch(input, init);
}
// Skip if no active config (not initialized yet)
const config = getActiveConfig();
if (!config) {
return originalFetch(input, init);
}
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
const url = new URL(request.url);
const spanName = `${request.method} ${url.host}`;
return tracer.startActiveSpan(
spanName,
{
kind: SpanKind.CLIENT,
attributes: gatherRequestAttributes(request),
},
async (span) => {
try {
// Inject trace context into request headers for distributed tracing
const shouldIncludeContext =
typeof config.fetch?.includeTraceContext === 'function'
? config.fetch.includeTraceContext(request)
: (config.fetch?.includeTraceContext ?? true);
if (shouldIncludeContext) {
propagation.inject(api_context.active(), request.headers, {
set: (headers, key, value) => {
if (typeof value === 'string') {
headers.set(key, value);
}
},
});
}
// Make the actual fetch call
const response = await originalFetch(request);
// Add response attributes
span.setAttributes(gatherResponseAttributes(response));
// Set span status based on response
if (response.ok) {
span.setStatus({ code: SpanStatusCode.OK });
} else {
span.setStatus({ code: SpanStatusCode.ERROR });
}
return response;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : String(error),
});
throw error;
} finally {
span.end();
}
},
);
};
// Replace global fetch
globalThis.fetch = instrumentedFetch as typeof fetch;
}