UNPKG

autotel

Version:
303 lines (301 loc) 9.69 kB
import { TraceFlags } from "@opentelemetry/api"; //#region src/messaging-adapters.ts /** * NATS JetStream adapter * * Captures NATS-specific attributes following NATS observability conventions. * * @example Producer * ```typescript * const publishOrder = traceProducer({ * system: 'nats', * destination: 'orders.created', * ...natsAdapter.producer, * })(ctx => async (subject, payload, opts) => { * const headers = ctx.getTraceHeaders(); * await nc.publish(subject, payload, { headers }); * }); * ``` * * @example Consumer * ```typescript * const processOrder = traceConsumer({ * system: 'nats', * destination: 'orders.created', * consumerGroup: 'order-processor', * ...natsAdapter.consumer, * })(ctx => async (msg: JsMsg) => { * await handleOrder(msg.data); * msg.ack(); * }); * ``` */ const natsAdapter = { producer: { customAttributes: (_ctx, args) => { const msg = args[0]; const attrs = {}; if (msg?.subject) attrs["nats.subject"] = msg.subject; if (msg?.replyTo) attrs["nats.reply_to"] = msg.replyTo; if (msg?.stream) attrs["nats.stream"] = msg.stream; return attrs; } }, consumer: { headersFrom: (msg) => { const headers = msg.headers; if (!headers) return; if (typeof headers.toJSON === "function") { const json = headers.toJSON(); if (json && typeof json === "object") return json; } if (typeof headers.get === "function") { const result = {}; for (const key of [ "traceparent", "tracestate", "baggage", "x-b3-traceid", "x-b3-spanid", "x-b3-sampled", "b3" ]) { const value = headers.get(key); if (value) result[key] = value; } if (Object.keys(result).length > 0) return result; } if (typeof headers.entries === "function") { const result = {}; for (const [key, value] of headers.entries()) if (typeof key === "string" && typeof value === "string") result[key] = value; if (Object.keys(result).length > 0) return result; } }, customAttributes: (_ctx, msg) => { const natsMsg = msg; const attrs = {}; if (natsMsg.subject) attrs["nats.subject"] = natsMsg.subject; if (natsMsg.reply) attrs["nats.reply_to"] = natsMsg.reply; if (natsMsg.info?.stream) attrs["nats.stream"] = natsMsg.info.stream; if (natsMsg.info?.consumer) attrs["nats.consumer"] = natsMsg.info.consumer; if (natsMsg.info?.redeliveryCount !== void 0) attrs["nats.delivered_count"] = natsMsg.info.redeliveryCount; if (natsMsg.info?.pending !== void 0) attrs["nats.pending"] = natsMsg.info.pending; return attrs; } } }; /** * Temporal adapter * * Captures Temporal-specific attributes for workflow activities. * Use this when instrumenting Temporal activity handlers. * * @example Activity handler * ```typescript * const processOrder = traceConsumer({ * system: 'temporal', * destination: 'order-activities', * ...temporalAdapter.consumer, * })(ctx => async (info: ActivityInfo, input: OrderInput) => { * // Temporal attributes are captured automatically * return processOrderLogic(input); * }); * ``` * * @example Workflow signal/query * ```typescript * const sendSignal = traceProducer({ * system: 'temporal', * destination: 'order-signals', * ...temporalAdapter.producer, * })(ctx => async (workflowId, signalName, payload) => { * await client.workflow.signal(workflowId, signalName, payload); * }); * ``` */ const temporalAdapter = { producer: { customAttributes: (_ctx, args) => { const info = args[0]; const attrs = {}; if (info?.workflowId) attrs["temporal.workflow_id"] = info.workflowId; if (info?.runId) attrs["temporal.run_id"] = info.runId; if (info?.taskQueue) attrs["temporal.task_queue"] = info.taskQueue; if (info?.workflowType) attrs["temporal.workflow_type"] = info.workflowType; return attrs; } }, consumer: { customAttributes: (_ctx, msg) => { const info = msg; const attrs = {}; if (info.workflowId) attrs["temporal.workflow_id"] = info.workflowId; if (info.runId) attrs["temporal.run_id"] = info.runId; if (info.activityId) attrs["temporal.activity_id"] = info.activityId; if (info.taskQueue) attrs["temporal.task_queue"] = info.taskQueue; if (info.attempt !== void 0) attrs["temporal.attempt"] = info.attempt; if (info.activityType) attrs["temporal.activity_type"] = info.activityType; return attrs; } } }; /** * Cloudflare Queues adapter * * Captures Cloudflare Queue-specific attributes. * * @example Queue consumer * ```typescript * export default { * async queue(batch: MessageBatch, env: Env) { * for (const msg of batch.messages) { * await processMessage(msg); * } * }, * }; * * const processMessage = traceConsumer({ * system: 'cloudflare_queues', * destination: 'my-queue', * ...cloudflareQueuesAdapter.consumer, * })(ctx => async (msg: Message) => { * await handleMessage(msg.body); * msg.ack(); * }); * ``` */ const cloudflareQueuesAdapter = { consumer: { customAttributes: (_ctx, msg) => { const cfMsg = msg; const attrs = {}; if (cfMsg.id) attrs["cloudflare.queue.message_id"] = cfMsg.id; if (cfMsg.timestamp) attrs["cloudflare.queue.timestamp_ms"] = cfMsg.timestamp.getTime(); if (cfMsg.attempts !== void 0) attrs["cloudflare.queue.attempts"] = cfMsg.attempts; return attrs; } } }; /** * Datadog trace context extractor * * Extracts parent span context from Datadog-format trace headers. * Converts Datadog's decimal IDs to OpenTelemetry's hex format. * * Note: Datadog sends trace/span IDs as decimal strings, not hex. * This extractor converts decimal -> hex before formatting for OTel. * * @example * ```typescript * const processMessage = traceConsumer({ * system: 'kafka', * destination: 'events', * customContextExtractor: datadogContextExtractor, * })(ctx => async (msg) => { * // Links to parent Datadog span automatically * }); * ``` */ function datadogContextExtractor(headers) { const traceIdDecimal = headers["x-datadog-trace-id"]; const spanIdDecimal = headers["x-datadog-parent-id"]; const samplingPriority = headers["x-datadog-sampling-priority"]; if (!traceIdDecimal || !spanIdDecimal) return null; let otelTraceId; let otelSpanId; try { otelTraceId = BigInt(traceIdDecimal).toString(16).padStart(32, "0"); otelSpanId = BigInt(spanIdDecimal).toString(16).padStart(16, "0"); } catch { return null; } return { traceId: otelTraceId, spanId: otelSpanId, traceFlags: (samplingPriority ? Number.parseInt(samplingPriority, 10) > 0 : true) ? TraceFlags.SAMPLED : TraceFlags.NONE, isRemote: true }; } /** * B3 (Zipkin) trace context extractor * * Extracts parent span context from B3 format headers. * Supports both single-header (b3) and multi-header formats. * * @see https://github.com/openzipkin/b3-propagation * * @example Single-header format * ```typescript * // Header: b3: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1 * const processMessage = traceConsumer({ * system: 'rabbitmq', * destination: 'events', * customContextExtractor: b3ContextExtractor, * })(ctx => async (msg) => { * // Links to parent Zipkin span * }); * ``` * * @example Multi-header format * ```typescript * // Headers: X-B3-TraceId, X-B3-SpanId, X-B3-Sampled * ``` */ function b3ContextExtractor(headers) { const b3Single = headers["b3"] || headers["B3"]; if (b3Single) { if (b3Single === "0") return null; const parts = b3Single.split("-"); const traceId = parts[0]; const spanId = parts[1]; const sampledFlag = parts[2]; if (traceId && spanId) { const sampled = sampledFlag !== "0" && sampledFlag !== "d"; return { traceId: traceId.padStart(32, "0"), spanId: spanId.padStart(16, "0"), traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, isRemote: true }; } } const traceId = headers["x-b3-traceid"] || headers["X-B3-TraceId"] || headers["X-B3-Traceid"]; const spanId = headers["x-b3-spanid"] || headers["X-B3-SpanId"] || headers["X-B3-Spanid"]; const sampledHeader = headers["x-b3-sampled"] || headers["X-B3-Sampled"] || headers["x-b3-flags"] || headers["X-B3-Flags"]; if (!traceId || !spanId) return null; const sampled = sampledHeader === "1" || sampledHeader === "true" || sampledHeader === void 0; return { traceId: traceId.padStart(32, "0"), spanId: spanId.padStart(16, "0"), traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, isRemote: true }; } /** * AWS X-Ray trace context extractor * * Extracts parent span context from AWS X-Ray trace header. * Format: Root=1-{timestamp}-{random};Parent={parent-id};Sampled={0|1} * * @example * ```typescript * const processMessage = traceConsumer({ * system: 'sqs', * destination: 'my-queue', * customContextExtractor: xrayContextExtractor, * })(ctx => async (msg) => { * // Links to parent X-Ray trace * }); * ``` */ function xrayContextExtractor(headers) { const xrayHeader = headers["x-amzn-trace-id"] || headers["X-Amzn-Trace-Id"]; if (!xrayHeader) return null; const rootMatch = xrayHeader.match(/Root=1-([a-f0-9]{8})-([a-f0-9]{24})/i); const parentMatch = xrayHeader.match(/Parent=([a-f0-9]{16})/i); const sampledMatch = xrayHeader.match(/Sampled=([01])/); if (!rootMatch || !parentMatch) return null; const timestamp = rootMatch[1]; const random = rootMatch[2]; const parentId = parentMatch[1]; if (!timestamp || !random || !parentId) return null; return { traceId: `${timestamp}${random}`, spanId: parentId, traceFlags: (sampledMatch ? sampledMatch[1] === "1" : true) ? TraceFlags.SAMPLED : TraceFlags.NONE, isRemote: true }; } //#endregion export { b3ContextExtractor, cloudflareQueuesAdapter, datadogContextExtractor, natsAdapter, temporalAdapter, xrayContextExtractor }; //# sourceMappingURL=messaging-adapters.js.map