UNPKG

autotel-cloudflare

Version:

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

281 lines (279 loc) 8.39 kB
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api'; import { resourceFromAttributes } from '@opentelemetry/resources'; import { createInitialiser, WorkerTracerProvider, WorkerTracer } from 'autotel-edge'; // src/agents/otel-observability.ts var activeSpans = /* @__PURE__ */ new Map(); var providerInitialized = false; function initProvider(config) { if (providerInitialized) return; 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-cloudflare", "telemetry.sdk.language": "js", "agent.framework": "cloudflare-agents" }); const provider = new WorkerTracerProvider(config.spanProcessors, resource); provider.register(); const tracer = trace.getTracer("autotel-cloudflare/agents"); tracer.setHeadSampler(config.sampling.headSampler); providerInitialized = true; } function getDefaultSpanName(event) { switch (event.type) { case "rpc": { return `agent.rpc ${event.payload.method}`; } case "schedule:create": { return `agent.schedule.create ${event.payload.callback}`; } case "schedule:execute": { return `agent.schedule.execute ${event.payload.callback}`; } case "schedule:cancel": { return `agent.schedule.cancel ${event.payload.callback}`; } case "connect": { return `agent.connect`; } case "destroy": { return `agent.destroy`; } case "state:update": { return `agent.state.update`; } case "message:request": { return `agent.message.request`; } case "message:response": { return `agent.message.response`; } case "message:clear": { return `agent.message.clear`; } case "mcp:client:preconnect": { return `mcp.preconnect ${event.payload.serverId}`; } case "mcp:client:connect": { return `mcp.connect ${event.payload.url}`; } case "mcp:client:authorize": { return `mcp.authorize ${event.payload.serverId}`; } case "mcp:client:discover": { return `mcp.discover`; } default: { return `agent.${event.type}`; } } } function getDefaultAttributes(event) { const attrs = { "agent.event.type": event.type, "agent.event.id": event.id }; switch (event.type) { case "rpc": { attrs["agent.rpc.method"] = event.payload.method; if (event.payload.streaming !== void 0) { attrs["agent.rpc.streaming"] = event.payload.streaming; } break; } case "schedule:create": case "schedule:execute": case "schedule:cancel": { attrs["agent.schedule.callback"] = event.payload.callback; attrs["agent.schedule.id"] = event.payload.id; break; } case "connect": { attrs["agent.connection.id"] = event.payload.connectionId; break; } case "mcp:client:preconnect": { attrs["agent.mcp.server_id"] = event.payload.serverId; break; } case "mcp:client:connect": { attrs["agent.mcp.url"] = event.payload.url; attrs["agent.mcp.transport"] = event.payload.transport; attrs["agent.mcp.state"] = event.payload.state; if (event.payload.error) { attrs["agent.mcp.error"] = event.payload.error; } break; } case "mcp:client:authorize": { attrs["agent.mcp.server_id"] = event.payload.serverId; attrs["agent.mcp.auth_url"] = event.payload.authUrl; if (event.payload.clientId) { attrs["agent.mcp.client_id"] = event.payload.clientId; } break; } } for (const [key, value] of Object.entries(event.payload)) { if (attrs[`agent.${key}`] === void 0 && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) { attrs[`agent.payload.${key}`] = value; } } return attrs; } function getSpanKind(event) { switch (event.type) { case "rpc": { return SpanKind.SERVER; } case "connect": { return SpanKind.SERVER; } case "mcp:client:connect": case "mcp:client:preconnect": case "mcp:client:authorize": case "mcp:client:discover": { return SpanKind.CLIENT; } default: { return SpanKind.INTERNAL; } } } function shouldTraceEvent(event, options) { const defaults = { traceRpc: true, traceSchedule: true, traceMcp: true, traceStateUpdates: false, traceMessages: true, traceLifecycle: true }; const opts = { ...defaults, ...options }; switch (event.type) { case "rpc": { return opts.traceRpc ?? true; } case "schedule:create": case "schedule:execute": case "schedule:cancel": { return opts.traceSchedule ?? true; } case "mcp:client:preconnect": case "mcp:client:connect": case "mcp:client:authorize": case "mcp:client:discover": { return opts.traceMcp ?? true; } case "state:update": { return opts.traceStateUpdates ?? false; } case "message:request": case "message:response": case "message:clear": { return opts.traceMessages ?? true; } case "connect": case "destroy": { return opts.traceLifecycle ?? true; } default: { return true; } } } async function exportSpans(traceId, ctx) { const tracer = trace.getTracer("autotel-cloudflare/agents"); if (tracer instanceof WorkerTracer) { try { if (ctx && "scheduler" in ctx) { const ctxWithScheduler = ctx; if (ctxWithScheduler.scheduler) { await ctxWithScheduler.scheduler.wait(1); } } await tracer.forceFlush(traceId); } catch (error) { console.error("[autotel-cloudflare/agents] Failed to export spans:", error); } } } var OtelObservability = class { config; options; initialized = false; constructor(config) { const initialiser = createInitialiser(config); this.config = initialiser({}, void 0); this.options = config.agents ?? {}; } /** * Initialize the tracer provider (called lazily on first emit) */ initialize() { if (this.initialized) return; initProvider(this.config); this.initialized = true; } /** * Emit an observability event * * Converts the event to an OpenTelemetry span based on the event type. */ emit(event, ctx) { this.initialize(); if (!shouldTraceEvent(event, this.options)) { return; } const tracer = trace.getTracer("autotel-cloudflare/agents"); const spanName = this.options.spanNameFormatter ? this.options.spanNameFormatter(event) : getDefaultSpanName(event); const defaultAttrs = getDefaultAttributes(event); const customAttrs = this.options.attributeExtractor ? this.options.attributeExtractor(event) : {}; const attributes = { ...defaultAttrs, ...customAttrs }; const kind = getSpanKind(event); const span = tracer.startSpan(spanName, { kind, attributes, startTime: event.timestamp }); span.setStatus({ code: SpanStatusCode.OK }); span.end(event.timestamp + 1); activeSpans.set(event.id, span); const traceId = span.spanContext().traceId; if (ctx && "waitUntil" in ctx && typeof ctx.waitUntil === "function") { ctx.waitUntil(exportSpans(traceId, ctx)); } else { void exportSpans(traceId, ctx); } } }; function createOtelObservability(config) { return new OtelObservability(config); } function createOtelObservabilityFromEnv(env, options) { const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT || void 0; const serviceName = env.OTEL_SERVICE_NAME || "cloudflare-agent"; let headers; const headersStr = env.OTEL_EXPORTER_OTLP_HEADERS; if (headersStr) { headers = {}; for (const pair of headersStr.split(",")) { const [key, value] = pair.split("="); if (key && value) { headers[key.trim()] = value.trim(); } } } const exporterUrl = endpoint || "http://localhost:4318/v1/traces"; return createOtelObservability({ service: { name: serviceName }, exporter: { url: exporterUrl, headers }, agents: options }); } export { OtelObservability, createOtelObservability, createOtelObservabilityFromEnv }; //# sourceMappingURL=agents.js.map //# sourceMappingURL=agents.js.map