UNPKG

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
/** * 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; }