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