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