autotel
Version:
Write Once, Observe Anywhere
303 lines (301 loc) • 9.69 kB
JavaScript
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