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
JavaScript
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