UNPKG

autotel-cloudflare

Version:

The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling

658 lines (652 loc) 21.7 kB
import { instrumentBindings } from './chunk-KAUHT25H.js'; export { instrumentAI, instrumentAnalyticsEngine, instrumentBindings, instrumentBrowserRendering, instrumentD1, instrumentHyperdrive, instrumentImages, instrumentKV, instrumentQueueProducer, instrumentR2, instrumentRateLimiter, instrumentServiceBinding, instrumentVectorize } from './chunk-KAUHT25H.js'; import { instrumentDO } from './chunk-MIDMNKDC.js'; export { instrumentDO, instrumentWorkflow } from './chunk-MIDMNKDC.js'; import { createWorkersLogger } from './chunk-RVVMMPWN.js'; export { createWorkersLogger, getActorLogger, getQueueLogger, getRequestLogger, getWorkflowLogger } from './chunk-RVVMMPWN.js'; import { wrap, unwrap, proxyExecutionContext } from './chunk-O4IYKWPJ.js'; import { createInitialiser, getActiveConfig, shouldInstrumentPath, setConfig, WorkerTracerProvider, WorkerTracer, getServiceForPath } from 'autotel-edge'; export * from 'autotel-edge'; import { trace, SpanKind, propagation, context, SpanStatusCode } from '@opentelemetry/api'; import { resourceFromAttributes } from '@opentelemetry/resources'; function gatherRequestAttributes(request) { 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") || void 0 }; } function gatherResponseAttributes(response) { return { "http.response.status_code": response.status, "http.response.body.size": response.headers.get("content-length") || void 0 }; } function instrumentGlobalFetch() { const originalFetch = globalThis.fetch; const instrumentedFetch = function fetch(input, init) { const request = new Request(input, init); if (!request.url.startsWith("http")) { return originalFetch(input, init); } const config = getActiveConfig(); if (!config) { return originalFetch(input, init); } const tracer = trace.getTracer("autotel-edge"); 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 { const shouldIncludeContext = typeof config.fetch?.includeTraceContext === "function" ? config.fetch.includeTraceContext(request) : config.fetch?.includeTraceContext ?? true; if (shouldIncludeContext) { propagation.inject(context.active(), request.headers, { set: (headers, key, value) => { if (typeof value === "string") { headers.set(key, value); } } }); } const response = await originalFetch(request); span.setAttributes(gatherResponseAttributes(response)); if (response.ok) { span.setStatus({ code: SpanStatusCode.OK }); } else { span.setStatus({ code: SpanStatusCode.ERROR }); } return response; } catch (error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } } ); }; globalThis.fetch = instrumentedFetch; } function sanitizeURL(url) { const u = new URL(url); return `${u.protocol}//${u.host}${u.pathname}`; } function instrumentCacheMethod(fn, cacheName, operation) { const handler = { async apply(target, thisArg, argArray) { const tracer = trace.getTracer("autotel-edge"); const firstArg = argArray[0]; const url = firstArg instanceof Request ? firstArg.url : typeof firstArg === "string" ? firstArg : void 0; const spanName = `Cache ${cacheName}.${operation}`; return tracer.startActiveSpan( spanName, { kind: SpanKind.CLIENT, attributes: { "cache.name": cacheName, "cache.operation": operation, "cache.key": url ? sanitizeURL(url) : void 0 } }, async (span) => { try { const result = await Reflect.apply(target, thisArg, argArray); if (operation === "match") { span.setAttribute("cache.hit", !!result); } span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } } ); } }; return wrap(fn, handler); } function instrumentCache(cache, cacheName) { const handler = { get(target, prop) { const value = Reflect.get(target, prop); if ((prop === "match" || prop === "put" || prop === "delete") && typeof value === "function") { return instrumentCacheMethod( value.bind(target), cacheName, prop ); } if (typeof value === "function") { return value.bind(target); } return value; } }; return wrap(cache, handler); } function instrumentCachesOpen(openFn) { const handler = { async apply(target, thisArg, argArray) { const cacheName = argArray[0]; const cache = await Reflect.apply(target, thisArg, argArray); return instrumentCache(cache, cacheName); } }; return wrap(openFn, handler); } function instrumentGlobalCache() { const handler = { get(target, prop) { if (prop === "default") { return instrumentCache(target.default, "default"); } else if (prop === "open") { const openFn = Reflect.get(target, prop); if (typeof openFn === "function") { return instrumentCachesOpen(openFn.bind(target)); } } return Reflect.get(target, prop); } }; globalThis.caches = wrap(caches, handler); } // src/wrappers/instrument.ts var headersGetter = { get: (carrier, key) => carrier.get(key) ?? void 0, keys: (carrier) => [...carrier.keys()] }; function extractCfAttributes(request) { const cf = request.cf; if (!cf) return {}; const attrs = {}; const set = (key, value) => { if (value !== void 0 && value !== null) { attrs[key] = value; } }; set("cloudflare.colo", cf.colo); const ray = request.headers.get("cf-ray"); if (ray) attrs["cloudflare.ray_id"] = ray; set("cloudflare.country", cf.country); set("cloudflare.city", cf.city); set("cloudflare.region", cf.region); set("cloudflare.continent", cf.continent); set("cloudflare.timezone", cf.timezone); set("cloudflare.latitude", cf.latitude); set("cloudflare.longitude", cf.longitude); set("cloudflare.asn", cf.asn); set("cloudflare.as_organization", cf.asOrganization); set("cloudflare.http_protocol", cf.httpProtocol); set("cloudflare.tls_version", cf.tlsVersion); set("cloudflare.client_tcp_rtt", cf.clientTcpRtt); return attrs; } function createFetchInstrumentation(config) { return { getInitialSpanInfo: (request) => { const url = new URL(request.url); const routeService = getServiceForPath( url.pathname, config.handlers.fetch.routes ); const cfAttrs = config.extractCfAttributes === false ? {} : extractCfAttributes(request); return { name: `${request.method} ${url.pathname}`, options: { kind: SpanKind.SERVER, attributes: { "http.request.method": request.method, "url.full": request.url, ...routeService ? { "service.name": routeService, "autotel.route.service": routeService } : {}, ...cfAttrs } }, context: propagation.extract(context.active(), request.headers, headersGetter) }; }, getAttributesFromResult: (response) => ({ "http.response.status_code": response.status }), executionSucces: (span, trigger, result) => { if (result.status >= 500) { span.setStatus({ code: SpanStatusCode.ERROR }); } if (config.handlers.fetch.postProcess) { const readableSpan = span; config.handlers.fetch.postProcess(span, { request: trigger, response: result, readable: readableSpan }); } } }; } var scheduledInstrumentation = { getInitialSpanInfo: (event) => { return { name: `scheduledHandler ${event.cron || "unknown"}`, options: { kind: SpanKind.INTERNAL, attributes: { "faas.trigger": "timer", "faas.cron": event.cron || "unknown", "faas.scheduled_time": new Date(event.scheduledTime).toISOString() } } }; } }; var MessageStatusCount = class { succeeded = 0; failed = 0; implicitly_acked = 0; implicitly_retried = 0; total; constructor(total) { this.total = total; } ack() { this.succeeded = this.succeeded + 1; } ackRemaining() { this.implicitly_acked = this.total - this.succeeded - this.failed; this.succeeded = this.total - this.failed; } retry() { this.failed = this.failed + 1; } retryRemaining() { this.implicitly_retried = this.total - this.succeeded - this.failed; this.failed = this.total - this.succeeded; } toAttributes() { return { "queue.messages_count": this.total, "queue.messages_success": this.succeeded, "queue.messages_failed": this.failed, "queue.batch_success": this.succeeded === this.total, "queue.implicitly_acked": this.implicitly_acked, "queue.implicitly_retried": this.implicitly_retried }; } }; function addQueueEvent(name, msg, delaySeconds) { const attrs = {}; if (msg) { attrs["queue.message_id"] = msg.id; attrs["queue.message_timestamp"] = msg.timestamp.toISOString(); if ("attempts" in msg && typeof msg.attempts === "number") { attrs["queue.message_attempts"] = msg.attempts; } } if (delaySeconds !== void 0) { attrs["queue.retry_delay_seconds"] = delaySeconds; } trace.getActiveSpan()?.addEvent(name, attrs); } function proxyQueueMessage(msg, count) { const msgHandler = { get: (target, prop) => { if (prop === "ack") { const ackFn = Reflect.get(target, prop); return new Proxy(ackFn, { apply: (fnTarget) => { addQueueEvent("messageAck", msg); count.ack(); Reflect.apply(fnTarget, msg, []); } }); } else if (prop === "retry") { const retryFn = Reflect.get(target, prop); return new Proxy(retryFn, { apply: (fnTarget, _thisArg, args) => { const retryOptions = args[0]; const delaySeconds = retryOptions?.delaySeconds; addQueueEvent("messageRetry", msg, delaySeconds); if (retryOptions?.contentType) { const span = trace.getActiveSpan(); if (span) { span.setAttribute("queue.message.content_type", retryOptions.contentType); } } count.retry(); const result = Reflect.apply(fnTarget, msg, args); return result; } }); } else { return Reflect.get(target, prop, msg); } } }; return wrap(msg, msgHandler); } function proxyMessageBatch(batch, count) { const batchHandler = { get: (target, prop) => { if (prop === "messages") { const messages = Reflect.get(target, prop); const messagesHandler = { get: (target2, prop2) => { if (typeof prop2 === "string" && !isNaN(parseInt(prop2))) { const message = Reflect.get(target2, prop2); return proxyQueueMessage(message, count); } else { return Reflect.get(target2, prop2); } } }; return wrap(messages, messagesHandler); } else if (prop === "ackAll") { const ackFn = Reflect.get(target, prop); return new Proxy(ackFn, { apply: (fnTarget) => { addQueueEvent("ackAll"); count.ackRemaining(); Reflect.apply(fnTarget, batch, []); } }); } else if (prop === "retryAll") { const retryFn = Reflect.get(target, prop); return new Proxy(retryFn, { apply: (fnTarget, _thisArg, args) => { const retryOptions = args[0]; const delaySeconds = retryOptions?.delaySeconds; addQueueEvent("retryAll", void 0, delaySeconds); count.retryRemaining(); Reflect.apply(fnTarget, batch, args); } }); } return Reflect.get(target, prop); } }; return wrap(batch, batchHandler); } var QueueInstrumentation = class { count; getInitialSpanInfo(batch) { return { name: `queueHandler ${batch.queue || "unknown"}`, options: { kind: SpanKind.CONSUMER, attributes: { "faas.trigger": "pubsub", "queue.name": batch.queue || "unknown" } } }; } instrumentTrigger(batch) { this.count = new MessageStatusCount(batch.messages.length); return proxyMessageBatch(batch, this.count); } executionSucces(span, _trigger, _result) { if (this.count) { this.count.ackRemaining(); span.setAttributes(this.count.toAttributes()); } } executionFailed(span, _trigger, _error) { if (this.count) { this.count.retryRemaining(); span.setAttributes(this.count.toAttributes()); } } }; function headerAttributes(message) { const attrs = {}; if (message.headers instanceof Headers) { const config = getActiveConfig(); const allowlist = config?.dataSafety?.emailHeaderAllowlist; for (const [key, value] of message.headers.entries()) { if (allowlist && !allowlist.includes(key.toLowerCase())) { continue; } attrs[`email.header.${key}`] = value; } } return attrs; } var emailInstrumentation = { getInitialSpanInfo: (message) => { const attributes = { "faas.trigger": "other", "messaging.destination.name": message.to || "unknown" }; if ("headers" in message && message.headers instanceof Headers) { const messageId = message.headers.get("Message-Id"); if (messageId) { attributes["rpc.message.id"] = messageId; } Object.assign(attributes, headerAttributes(message)); } return { name: `emailHandler ${message.to || "unknown"}`, options: { kind: SpanKind.CONSUMER, attributes } }; } }; async function exportSpans(traceId, tracker, ctx) { const tracer = trace.getTracer("autotel-edge"); if (tracer instanceof WorkerTracer) { try { const ctxWithScheduler = ctx; if (ctxWithScheduler.scheduler) { await ctxWithScheduler.scheduler.wait(1); } await tracker?.wait(); await tracer.forceFlush(traceId); } catch (error) { console.error("[autotel-edge] Failed to export spans:", error); } } } function createHandlerFlow(instrumentation) { return (handlerFn, [trigger, env, context$1]) => { const { ctx: proxiedCtx, tracker } = proxyExecutionContext(context$1); const tracer = trace.getTracer("autotel-edge"); const { name, options, context: spanContext } = instrumentation.getInitialSpanInfo(trigger); if (options.attributes) { options.attributes["faas.coldstart"] = coldStart; } else { options.attributes = { "faas.coldstart": coldStart }; } coldStart = false; const parentContext = spanContext || context.active(); const instrumentedTrigger = instrumentation.instrumentTrigger ? instrumentation.instrumentTrigger(trigger) : trigger; return tracer.startActiveSpan(name, options, parentContext, async (span) => { try { const result = await handlerFn(instrumentedTrigger, env, proxiedCtx); if (instrumentation.getAttributesFromResult) { const attributes = instrumentation.getAttributesFromResult(result); span.setAttributes(attributes); } span.setStatus({ code: SpanStatusCode.OK }); if (instrumentation.executionSucces) { instrumentation.executionSucces(span, trigger, result); } return result; } catch (error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); if (instrumentation.executionFailed) { instrumentation.executionFailed(span, trigger, error); } throw error; } finally { span.end(); context$1.waitUntil(exportSpans(span.spanContext().traceId, tracker, context$1)); } }); }; } function createHandlerProxy(_handler, handlerFn, initialiser, instrumentation) { return (trigger, env, ctx) => { const config = initialiser(env, trigger); if (config.instrumentation.disabled) { return handlerFn(trigger, env, ctx); } const instrumentedEnv = instrumentBindings(env); const configContext = setConfig(config); initProvider(config); const flowFn = createHandlerFlow(instrumentation); return context.with(configContext, () => { return flowFn(handlerFn, [trigger, instrumentedEnv, ctx]); }); }; } function createHandlerProxyWithConfig(_handler, handlerFn, initialiser, createInstrumentation) { return (trigger, env, ctx) => { const config = initialiser(env, trigger); if (config.instrumentation.disabled) { return handlerFn(trigger, env, ctx); } if (trigger instanceof Request) { const pathname = new URL(trigger.url).pathname; const fetchCfg = config.handlers.fetch; if (!shouldInstrumentPath(pathname, { include: fetchCfg.include, exclude: fetchCfg.exclude })) { return handlerFn(trigger, env, ctx); } } const instrumentedEnv = instrumentBindings(env); const configContext = setConfig(config); initProvider(config); const instrumentation = createInstrumentation(config); const flowFn = createHandlerFlow(instrumentation); return context.with(configContext, () => { return flowFn(handlerFn, [trigger, instrumentedEnv, ctx]); }); }; } var providerInitialized = false; var coldStart = true; function initProvider(config) { if (providerInitialized) return; if (config.instrumentation.instrumentGlobalFetch) { instrumentGlobalFetch(); } if (config.instrumentation.instrumentGlobalCache) { instrumentGlobalCache(); } propagation.setGlobalPropagator(config.propagator); const resource = resourceFromAttributes({ "service.name": config.service.name, "service.version": config.service.version, "service.namespace": config.service.namespace, "cloud.provider": "cloudflare", "cloud.platform": "cloudflare.workers", "telemetry.sdk.name": "autotel-edge", "telemetry.sdk.language": "js" }); const provider = new WorkerTracerProvider(config.spanProcessors, resource); provider.register(); const tracer = trace.getTracer("autotel-edge"); tracer.setHeadSampler(config.sampling.headSampler); providerInitialized = true; } function instrument(handler, config) { const initialiser = createInitialiser(config); if (handler.fetch) { const fetcher = unwrap(handler.fetch); handler.fetch = createHandlerProxyWithConfig( handler, fetcher, initialiser, createFetchInstrumentation ); } if (handler.scheduled) { const scheduled = unwrap(handler.scheduled); handler.scheduled = createHandlerProxy( handler, scheduled, initialiser, scheduledInstrumentation ); } if (handler.queue) { const queue = unwrap(handler.queue); handler.queue = createHandlerProxy( handler, queue, initialiser, new QueueInstrumentation() ); } if (handler.email) { const email = unwrap(handler.email); handler.email = createHandlerProxy( handler, email, initialiser, emailInstrumentation ); } return handler; } // src/wrappers/wrap-module.ts function wrapModule(config, handler) { return instrument(handler, config); } // src/wrappers/wrap-do.ts function wrapDurableObject(config, doClass) { return instrumentDO(doClass, config); } // src/wrappers/define-worker-fetch.ts function defineWorkerFetch(config, handler, loggerOptions = {}) { const wrapped = instrument( { fetch(request, env, ctx) { const log = createWorkersLogger(request, loggerOptions); return handler(request, env, ctx, log); } }, config ); return { fetch(request, env, ctx) { return Promise.resolve( wrapped.fetch(request, env, ctx) ); } }; } export { defineWorkerFetch, instrument, instrumentGlobalCache, instrumentGlobalFetch, wrapDurableObject, wrapModule }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map