UNPKG

autotel-cloudflare

Version:

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

383 lines (380 loc) 14.7 kB
import { c as asString, g as readProperty, h as readPath, o as asScalar, p as nonEmptyString, r as asFunction, s as asScalarArray } from "./values-BC6rdpGG.js"; import { t as workerTracer } from "./tracer-z8zwRBVS.js"; import { WorkerTracer, WorkerTracerProvider, createInitialiser } from "autotel-edge"; import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import { resourceFromAttributes } from "@opentelemetry/resources"; import { agentContextFromSpan, recordHumanApproval } from "autotel-genai/agent"; //#region src/agents/otel-observability.ts /** * OpenTelemetry-based Observability implementation for Cloudflare Agents SDK * * Converts Agent events into OpenTelemetry spans for distributed tracing. */ let 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" }); new WorkerTracerProvider(config.spanProcessors, resource).register(); workerTracer("autotel-cloudflare/agents").setHeadSampler(config.sampling.headSampler); providerInitialized = true; } function classifyEvent(type) { if (type.startsWith("mcp:")) return "mcp"; if (type.startsWith("workflow:")) return "workflow"; if (type.startsWith("fiber:")) return "fiber"; if (type.startsWith("agent_tool:")) return "agentTool"; if (type.startsWith("chat:transcript:")) return "transcript"; if (type.startsWith("chat:")) return "chat"; if (type.startsWith("submission:")) return "submission"; if (type.startsWith("queue:")) return "queue"; if (type.startsWith("schedule:")) return "schedule"; if (type.startsWith("message:") || type.startsWith("tool:") || type === "rpc:error") return "message"; if (type === "rpc") return "rpc"; if (type.startsWith("state:")) return "state"; if (type.startsWith("email:")) return "email"; if (type === "connect" || type === "disconnect" || type === "destroy") return "lifecycle"; return "message"; } function shouldTraceEvent(event, options) { const merged = { traceRpc: true, traceSchedule: true, traceQueue: true, traceSubmissions: true, traceMcp: true, traceStateUpdates: false, traceMessages: true, traceLifecycle: true, traceChat: true, traceTranscripts: true, traceFibers: true, traceToolRecovery: true, traceWorkflow: true, traceEmail: true, ...options }; switch (classifyEvent(event.type)) { case "rpc": return merged.traceRpc; case "schedule": return merged.traceSchedule; case "queue": return merged.traceQueue; case "submission": return merged.traceSubmissions; case "mcp": return merged.traceMcp; case "state": return merged.traceStateUpdates; case "message": return merged.traceMessages; case "lifecycle": return merged.traceLifecycle; case "chat": return merged.traceChat; case "transcript": return merged.traceTranscripts; case "fiber": return merged.traceFibers; case "agentTool": return merged.traceToolRecovery; case "workflow": return merged.traceWorkflow; case "email": return merged.traceEmail; } } function formatTypeSegment(value) { return value.replaceAll(/[:_]/g, "."); } function getPayloadName(payload, ...keys) { for (const key of keys) { const value = nonEmptyString(payload[key]); if (value !== void 0) return value; } } function getDefaultSpanName(event) { const payload = event.payload; switch (event.type) { case "rpc": case "rpc:error": return `agent.rpc ${asString(readProperty(payload, "method"))}`; case "schedule:create": case "schedule:execute": case "schedule:cancel": case "schedule:retry": case "schedule:error": case "schedule:duplicate_warning": return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, "callback") ?? "unknown"}`; case "queue:create": case "queue:retry": case "queue:error": return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, "callback") ?? "unknown"}`; case "submission:create": case "submission:status": case "submission:error": return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, "submissionId") ?? "unknown"}`; case "connect": case "disconnect": return `agent.${event.type} ${getPayloadName(payload, "connectionId") ?? "unknown"}`; case "mcp:client:preconnect": return `mcp.client.preconnect ${getPayloadName(payload, "serverId") ?? "unknown"}`; case "mcp:client:authorize": return `mcp.client.authorize ${getPayloadName(payload, "serverId") ?? "unknown"}`; case "mcp:client:connect": case "mcp:client:discover": case "mcp:client:close": return `mcp.client.${formatTypeSegment(event.type).split(".").slice(-1)[0]} ${getPayloadName(payload, "url", "capability", "state") ?? "unknown"}`; case "workflow:start": case "workflow:event": case "workflow:approved": case "workflow:rejected": case "workflow:terminated": case "workflow:paused": case "workflow:resumed": case "workflow:restarted": return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, "workflowName", "workflowId", "eventType") ?? "unknown"}`; case "email:receive": case "email:reply": case "email:send": return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, "subject") ?? "message"}`; case "destroy": return "agent.destroy"; default: return `agent.${formatTypeSegment(event.type)}`; } } function setIfPresent(attrs, key, value) { const attribute = asScalar(value) ?? asScalarArray(value); if (attribute !== void 0) attrs[key] = attribute; } function getDefaultAttributes(event) { const attrs = { "agent.event.type": event.type }; setIfPresent(attrs, "agent.event.id", event.id); setIfPresent(attrs, "agent.display_message", event.displayMessage); setIfPresent(attrs, "agent.class", event.agent); setIfPresent(attrs, "agent.instance.name", event.name); setIfPresent(attrs, "gen_ai.agent.name", event.agent); switch (event.type) { case "rpc": attrs["agent.rpc.method"] = event.payload.method; setIfPresent(attrs, "agent.rpc.streaming", event.payload.streaming); break; case "rpc:error": attrs["agent.rpc.method"] = event.payload.method; break; case "schedule:create": case "schedule:execute": case "schedule:cancel": case "schedule:retry": case "schedule:error": case "queue:create": case "queue:retry": case "queue:error": setIfPresent(attrs, "agent.schedule.callback", event.payload.callback); setIfPresent(attrs, "agent.schedule.id", event.payload.id); break; case "schedule:duplicate_warning": setIfPresent(attrs, "agent.schedule.callback", event.payload.callback); break; case "connect": case "disconnect": setIfPresent(attrs, "agent.connection.id", event.payload.connectionId); break; case "mcp:client:preconnect": setIfPresent(attrs, "agent.mcp.server_id", event.payload.serverId); break; case "mcp:client:connect": case "mcp:client:close": setIfPresent(attrs, "agent.mcp.url", event.payload.url); setIfPresent(attrs, "agent.mcp.transport", event.payload.transport); setIfPresent(attrs, "agent.mcp.state", event.payload.state); break; case "mcp:client:authorize": setIfPresent(attrs, "agent.mcp.server_id", event.payload.serverId); setIfPresent(attrs, "agent.mcp.auth_url", event.payload.authUrl); setIfPresent(attrs, "agent.mcp.client_id", event.payload.clientId); break; case "mcp:client:discover": setIfPresent(attrs, "agent.mcp.url", event.payload.url); setIfPresent(attrs, "agent.mcp.state", event.payload.state); setIfPresent(attrs, "agent.mcp.capability", event.payload.capability); break; } for (const [key, value] of Object.entries(event.payload)) setIfPresent(attrs, `agent.payload.${key}`, value); return attrs; } function getSpanKind(event) { switch (classifyEvent(event.type)) { case "rpc": case "lifecycle": case "email": return SpanKind.SERVER; case "mcp": return SpanKind.CLIENT; case "schedule": case "queue": return SpanKind.CONSUMER; default: return SpanKind.INTERNAL; } } function inferErrorMessage(event) { const direct = nonEmptyString(readProperty(event.payload, "error")); if (direct !== void 0) return direct; if (event.type === "workflow:rejected") return nonEmptyString(readProperty(event.payload, "reason")) ?? "workflow rejected"; if (event.type.endsWith(":failed") || event.type.endsWith(":error")) return event.type; } function isErrorEvent(event) { if (event.type === "workflow:rejected") return true; if (event.type.endsWith(":error") || event.type.endsWith(":failed")) return true; return asString(readProperty(event.payload, "error")) !== void 0; } function applyEventStatus(span, event) { if (!isErrorEvent(event)) { span.setStatus({ code: SpanStatusCode.OK }); return; } const message = inferErrorMessage(event); span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("error", true); if (message) span.setAttribute("exception.message", message); } async function exportSpans(traceId, ctx) { const tracer = trace.getTracer("autotel-cloudflare/agents"); if (tracer instanceof WorkerTracer) try { const wait = asFunction(readPath(ctx, "scheduler", "wait")); if (wait) await wait(1); await tracer.forceFlush(traceId); } catch (error) { console.error("[autotel-cloudflare/agents] Failed to export spans:", error); } } /** * OpenTelemetry-based Observability implementation * * Implements the Agents SDK Observability interface and converts * events into OpenTelemetry spans. */ var OtelObservability = class { config; options; initialized = false; constructor(config) { const initialiser = createInitialiser(config); this.config = initialiser({}, void 0); this.options = config.agents ?? {}; } initialize() { if (this.initialized) return; initProvider(this.config); this.initialized = true; } 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 span = tracer.startSpan(spanName, { kind: getSpanKind(event), attributes: { ...defaultAttrs, ...customAttrs }, startTime: event.timestamp }); if (event.type === "tool:result") { span.setAttribute("tool.name", event.payload.toolName); span.setAttribute("tool.call.id", event.payload.toolCallId); span.setAttribute("agent.outcome", isErrorEvent(event) ? "failure" : "success"); } if (event.type === "tool:approval") recordHumanApproval({ ctx: agentContextFromSpan(span), toolCallId: event.payload.toolCallId, approved: event.payload.approved, required: true, evidence: "observed" }); applyEventStatus(span, event); span.end(event.timestamp + 1); const traceId = span.spanContext().traceId; const waitUntil = asFunction(readProperty(ctx, "waitUntil")); if (waitUntil) { waitUntil(exportSpans(traceId, ctx)); return; } exportSpans(traceId, ctx); } }; function createOtelObservability(config) { return new OtelObservability(config); } function createOtelObservabilityFromEnv(env, options) { const endpoint = nonEmptyString(env.OTEL_EXPORTER_OTLP_ENDPOINT); const serviceName = nonEmptyString(env.OTEL_SERVICE_NAME) ?? "cloudflare-agent"; let headers; const headersStr = nonEmptyString(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(); } } return createOtelObservability({ service: { name: serviceName }, exporter: { url: endpoint || "http://localhost:4318/v1/traces", headers }, agents: options }); } //#endregion //#region src/agents/observability.ts const listeners = { state: /* @__PURE__ */ new Set(), rpc: /* @__PURE__ */ new Set(), message: /* @__PURE__ */ new Set(), chat: /* @__PURE__ */ new Set(), transcript: /* @__PURE__ */ new Set(), fiber: /* @__PURE__ */ new Set(), agentTool: /* @__PURE__ */ new Set(), schedule: /* @__PURE__ */ new Set(), lifecycle: /* @__PURE__ */ new Set(), workflow: /* @__PURE__ */ new Set(), mcp: /* @__PURE__ */ new Set(), email: /* @__PURE__ */ new Set() }; function createChannel(key, name) { return { name, publish(event) { for (const callback of listeners[key]) callback(event); } }; } const channels = { state: createChannel("state", "agents:state"), rpc: createChannel("rpc", "agents:rpc"), message: createChannel("message", "agents:message"), chat: createChannel("chat", "agents:chat"), transcript: createChannel("transcript", "agents:transcript"), fiber: createChannel("fiber", "agents:fiber"), agentTool: createChannel("agentTool", "agents:agent_tool"), schedule: createChannel("schedule", "agents:schedule"), lifecycle: createChannel("lifecycle", "agents:lifecycle"), workflow: createChannel("workflow", "agents:workflow"), mcp: createChannel("mcp", "agents:mcp"), email: createChannel("email", "agents:email") }; function getChannel(type) { if (type.startsWith("mcp:")) return channels.mcp; if (type.startsWith("workflow:")) return channels.workflow; if (type.startsWith("fiber:")) return channels.fiber; if (type.startsWith("transcript:") || type.startsWith("chat:transcript:")) return channels.transcript; if (type.startsWith("chat:")) return channels.chat; if (type.startsWith("agent_tool:")) return channels.agentTool; if (type.startsWith("schedule:") || type.startsWith("queue:")) return channels.schedule; if (type.startsWith("message:") || type.startsWith("tool:") || type.startsWith("submission:")) return channels.message; if (type === "rpc" || type.startsWith("rpc:")) return channels.rpc; if (type.startsWith("state:")) return channels.state; if (type.startsWith("email:")) return channels.email; return channels.lifecycle; } const genericObservability = { emit(event) { getChannel(event.type).publish(event); } }; function subscribe(channelKey, callback) { const bucket = listeners[channelKey]; bucket.add(callback); return () => { bucket.delete(callback); }; } //#endregion export { OtelObservability, channels, createOtelObservability, createOtelObservabilityFromEnv, genericObservability, subscribe };