autotel
Version:
Write Once, Observe Anywhere
760 lines (758 loc) • 29.6 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_sampling = require('./sampling.cjs');
const require_functional = require('./functional-C8B0Qa7o.cjs');
const require_correlated_events = require('./correlated-events-kSwLo3mi.cjs');
let _opentelemetry_api = require("@opentelemetry/api");
//#region src/messaging.ts
/**
* Messaging helpers for event-driven architectures
*
* Provides specialized tracing for message producers and consumers
* with automatic context propagation, link extraction, and OTel
* semantic convention compliance.
*
* @example Producer
* ```typescript
* import { traceProducer } from 'autotel/messaging';
*
* export const publishEvent = traceProducer({
* system: 'kafka',
* destination: 'user-events',
* })(ctx => async (event: UserEvent) => {
* const headers = ctx.getTraceHeaders();
* await producer.send({
* topic: 'user-events',
* messages: [{ value: JSON.stringify(event), headers }]
* });
* });
* ```
*
* @example Consumer
* ```typescript
* import { traceConsumer } from 'autotel/messaging';
*
* export const processEvents = traceConsumer({
* system: 'kafka',
* destination: 'user-events',
* consumerGroup: 'event-processor',
* batchMode: true,
* })(ctx => async (messages: KafkaMessage[]) => {
* // Links to producer spans are automatically extracted
* for (const msg of messages) {
* await processMessage(msg);
* }
* });
* ```
*
* @module
*/
/**
* Create a traced message producer function
*
* Sets SpanKind.PRODUCER, OTel messaging semantic attributes,
* and provides context injection helpers.
*
* @param config - Producer configuration
* @returns Factory function that wraps your producer logic
*
* @example Kafka producer
* ```typescript
* export const publishUserEvent = traceProducer({
* system: 'kafka',
* destination: 'user-events',
* messageIdFrom: (args) => args[0]?.eventId,
* })(ctx => async (event: UserEvent) => {
* const headers = ctx.getTraceHeaders();
* await producer.send({
* topic: 'user-events',
* messages: [{
* key: event.userId,
* value: JSON.stringify(event),
* headers,
* }]
* });
* });
* ```
*
* @example SQS producer
* ```typescript
* export const sendToSQS = traceProducer({
* system: 'sqs',
* destination: 'orders-queue',
* })(ctx => async (order: Order) => {
* const headers = ctx.getAllPropagationHeaders();
* await sqs.sendMessage({
* QueueUrl: QUEUE_URL,
* MessageBody: JSON.stringify(order),
* MessageAttributes: headersToSQSAttributes(headers),
* });
* });
* ```
*/
function traceProducer(config) {
const spanName = `${config.system}.publish ${config.destination}`;
return (fnFactory) => {
return require_functional.trace({
name: spanName,
spanKind: _opentelemetry_api.SpanKind.PRODUCER
}, (baseCtx) => {
const ctx = extendContextForProducer(baseCtx, config);
setProducerAttributes(ctx, config);
return (...args) => {
setDynamicProducerAttributes(ctx, config, args);
if (config.customAttributes) {
const customAttrs = config.customAttributes(ctx, args);
for (const [key, value] of Object.entries(customAttrs)) if (value !== void 0 && value !== null) ctx.setAttribute(key, value);
}
if (config.beforeSend) config.beforeSend(ctx, args);
return fnFactory(ctx)(...args).catch((error) => {
if (config.onError) config.onError(error, ctx);
throw error;
});
};
});
};
}
/**
* Create a traced message consumer function
*
* Sets SpanKind.CONSUMER, OTel messaging semantic attributes,
* automatically extracts links from producer trace headers,
* and provides DLQ/retry recording helpers.
*
* @param config - Consumer configuration
* @returns Factory function that wraps your consumer logic
*
* @example Kafka consumer (single message)
* ```typescript
* export const processUserEvent = traceConsumer({
* system: 'kafka',
* destination: 'user-events',
* consumerGroup: 'event-processor',
* headersFrom: (msg) => msg.headers,
* })(ctx => async (message: KafkaMessage) => {
* // Link to producer span is automatically created
* const event = JSON.parse(message.value.toString());
* await processEvent(event);
* });
* ```
*
* @example Kafka consumer (batch mode)
* ```typescript
* export const processUserEventBatch = traceConsumer({
* system: 'kafka',
* destination: 'user-events',
* consumerGroup: 'event-processor',
* batchMode: true,
* headersFrom: (msg) => msg.headers,
* lagMetrics: {
* getCurrentOffset: (msg) => msg.offset,
* getEndOffset: () => consumer.getHighWatermark(),
* getPartition: (msg) => msg.partition,
* },
* })(ctx => async (messages: KafkaMessage[]) => {
* // Links to all producer spans are automatically created
* for (const msg of messages) {
* await processEvent(JSON.parse(msg.value.toString()));
* }
* });
* ```
*
* @example SQS consumer with DLQ handling
* ```typescript
* export const processSQSMessage = traceConsumer({
* system: 'sqs',
* destination: 'orders-queue',
* headersFrom: (msg) => sqsAttributesToHeaders(msg.MessageAttributes),
* onDLQ: (ctx, reason) => {
* ctx.recordDLQ(reason, 'orders-dlq');
* },
* })(ctx => async (message: SQSMessage) => {
* try {
* await processOrder(JSON.parse(message.Body));
* } catch (error) {
* if (message.ApproximateReceiveCount > 3) {
* ctx.recordDLQ(error.message);
* throw error;
* }
* ctx.recordRetry(message.ApproximateReceiveCount, 3);
* throw error;
* }
* });
* ```
*/
function traceConsumer(config) {
const operation = config.batchMode ? "receive" : "process";
const spanName = `${config.system}.${operation} ${config.destination}`;
return (fnFactory) => {
return require_functional.trace({
name: spanName,
spanKind: _opentelemetry_api.SpanKind.CONSUMER
}, (baseCtx) => {
const linkStorage = { links: [] };
const orderingState = {
sequenceNumber: null,
partitionKey: null,
messageId: null,
isDuplicate: false,
outOfOrderInfo: null
};
const groupTracking = config.consumerGroupTracking;
const ctx = extendContextForConsumer(baseCtx, config, linkStorage, orderingState, {
memberId: typeof groupTracking?.memberId === "function" ? groupTracking.memberId() ?? null : groupTracking?.memberId ?? null,
groupInstanceId: typeof groupTracking?.groupInstanceId === "function" ? groupTracking.groupInstanceId() ?? null : groupTracking?.groupInstanceId ?? null,
assignedPartitions: [],
generation: null,
isActive: true,
lastHeartbeat: null,
state: null
});
setConsumerAttributes(ctx, config);
return async (...args) => {
await extractAndAddLinks(ctx, config, args, linkStorage);
if (config.ordering) extractAndProcessOrdering(ctx, config, args, orderingState);
if (config.lagMetrics) await extractLagMetrics(ctx, config.lagMetrics, args);
if (config.customAttributes) {
const batch = args[0];
const msg = config.batchMode && Array.isArray(batch) && batch.length > 0 ? batch[0] : batch;
if (msg !== void 0) {
const customAttrs = config.customAttributes(ctx, msg);
for (const [key, value] of Object.entries(customAttrs)) if (value !== void 0 && value !== null) ctx.setAttribute(key, value);
}
}
return fnFactory(ctx)(...args).catch((error) => {
if (config.onError) config.onError(error, ctx);
throw error;
});
};
});
};
}
/**
* Extend base context with producer-specific methods
*/
function extendContextForProducer(baseCtx, config) {
const producerCtx = {
...baseCtx,
getTraceHeaders() {
const headers = {};
_opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), headers);
const result = { traceparent: headers["traceparent"] || "" };
if (headers["tracestate"]) result.tracestate = headers["tracestate"];
return result;
},
getAllPropagationHeaders() {
const headers = {};
_opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), headers);
if (config.propagateBaggage) {
const baggage = _opentelemetry_api.propagation.getBaggage(_opentelemetry_api.context.active());
if (baggage) {
const entries = [];
for (const [key, value] of baggage.getAllEntries()) entries.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.value)}`);
if (entries.length > 0) headers["baggage"] = entries.join(",");
}
}
return headers;
},
getFullHeaders() {
const headers = producerCtx.getAllPropagationHeaders();
if (config.customHeaders) {
const customHeaders = config.customHeaders(producerCtx);
Object.assign(headers, customHeaders);
}
return headers;
}
};
return producerCtx;
}
/**
* Global sequence tracker for out-of-order detection (per partition)
*/
const sequenceTrackers = /* @__PURE__ */ new Map();
/**
* Global deduplication window (LRU-style using Map insertion order)
*/
const deduplicationWindow = /* @__PURE__ */ new Map();
const DEFAULT_DEDUP_WINDOW_SIZE = 1e3;
/**
* Clean up old entries from deduplication window
*/
function trimDeduplicationWindow(maxSize) {
if (deduplicationWindow.size > maxSize) {
const excess = deduplicationWindow.size - maxSize;
const iterator = deduplicationWindow.keys();
for (let i = 0; i < excess; i++) {
const key = iterator.next().value;
if (key) deduplicationWindow.delete(key);
}
}
}
/**
* Extend base context with consumer-specific methods
*/
function extendContextForConsumer(baseCtx, config, linkStorage, orderingState, groupState) {
const consumerCtx = {
...baseCtx,
recordDLQ(reason, dlqNameOrOptions, optionsParam) {
let dlqName;
let options;
if (typeof dlqNameOrOptions === "string") {
dlqName = dlqNameOrOptions;
options = optionsParam;
} else if (typeof dlqNameOrOptions === "object") options = dlqNameOrOptions;
const linkToProducer = options?.linkToProducer ?? true;
baseCtx.setAttribute("messaging.dlq.reason", reason);
if (dlqName) baseCtx.setAttribute("messaging.dlq.name", dlqName);
if (options?.reasonCategory) baseCtx.setAttribute("messaging.dlq.reason_category", options.reasonCategory);
if (options?.attemptCount !== void 0) baseCtx.setAttribute("messaging.dlq.attempt_count", options.attemptCount);
if (options?.originalError) {
baseCtx.setAttribute("messaging.dlq.error.type", options.originalError.name);
baseCtx.setAttribute("messaging.dlq.error.message", options.originalError.message);
}
if (options?.metadata) for (const [key, value] of Object.entries(options.metadata)) baseCtx.setAttribute(`messaging.dlq.metadata.${key}`, value);
const producerLink = linkStorage.links[0];
if (linkToProducer && producerLink) {
baseCtx.setAttribute("messaging.dlq.producer_trace_id", producerLink.context.traceId);
baseCtx.setAttribute("messaging.dlq.producer_span_id", producerLink.context.spanId);
}
const eventAttrs = {
"messaging.dlq.reason": reason,
...dlqName && { "messaging.dlq.name": dlqName },
...options?.reasonCategory && { "messaging.dlq.reason_category": options.reasonCategory },
...options?.attemptCount !== void 0 && { "messaging.dlq.attempt_count": options.attemptCount },
...options?.originalError && {
"messaging.dlq.error.type": options.originalError.name,
"messaging.dlq.error.message": options.originalError.message
}
};
if (linkToProducer && producerLink) {
eventAttrs["messaging.dlq.producer_trace_id"] = producerLink.context.traceId;
eventAttrs["messaging.dlq.producer_span_id"] = producerLink.context.spanId;
}
require_correlated_events.emitCorrelatedEvent(baseCtx, "dlq_routed", eventAttrs);
if (config.onDLQ) config.onDLQ(consumerCtx, reason);
},
recordReplay(options) {
baseCtx.setAttribute("messaging.replay", true);
if (options?.replayAttempt !== void 0) baseCtx.setAttribute("messaging.replay.attempt", options.replayAttempt);
if (options?.dlqDwellTimeMs !== void 0) baseCtx.setAttribute("messaging.replay.dwell_time_ms", options.dlqDwellTimeMs);
if (options?.originalDLQSpanContext) baseCtx.addLinks([{
context: options.originalDLQSpanContext,
attributes: { "messaging.link.source": "dlq_replay" }
}]);
require_correlated_events.emitCorrelatedEvent(baseCtx, "dlq_replay", {
"messaging.replay": true,
...options?.replayAttempt !== void 0 && { "messaging.replay.attempt": options.replayAttempt },
...options?.dlqDwellTimeMs !== void 0 && { "messaging.replay.dwell_time_ms": options.dlqDwellTimeMs }
});
},
recordRetry(attemptNumber, maxAttempts) {
baseCtx.setAttribute("messaging.retry.count", attemptNumber);
if (maxAttempts !== void 0) baseCtx.setAttribute("messaging.retry.max_attempts", maxAttempts);
require_correlated_events.emitCorrelatedEvent(baseCtx, "retry_attempt", {
"messaging.retry.count": attemptNumber,
...maxAttempts !== void 0 && { "messaging.retry.max_attempts": maxAttempts }
});
},
getProducerLinks() {
return [...linkStorage.links];
},
isDuplicate() {
return orderingState.isDuplicate;
},
getOutOfOrderInfo() {
return orderingState.outOfOrderInfo;
},
getSequenceNumber() {
return orderingState.sequenceNumber;
},
getPartitionKey() {
return orderingState.partitionKey;
},
recordRebalance(event) {
if (event.type === "assigned") {
groupState.assignedPartitions = event.partitions;
groupState.isActive = true;
groupState.state = "stable";
} else if (event.type === "revoked" || event.type === "lost") {
const revokedSet = new Set(event.partitions.map((p) => `${p.topic}:${p.partition}`));
groupState.assignedPartitions = groupState.assignedPartitions.filter((p) => !revokedSet.has(`${p.topic}:${p.partition}`));
if (event.type === "lost") {
groupState.isActive = false;
groupState.state = "dead";
} else groupState.state = groupState.assignedPartitions.length === 0 ? "empty" : "preparing_rebalance";
}
if (event.generation !== void 0) groupState.generation = event.generation;
if (event.memberId) groupState.memberId = event.memberId;
baseCtx.setAttribute("messaging.consumer_group.rebalance.type", event.type);
baseCtx.setAttribute("messaging.consumer_group.rebalance.partition_count", event.partitions.length);
if (event.generation !== void 0) baseCtx.setAttribute("messaging.consumer_group.generation", event.generation);
if (event.memberId) baseCtx.setAttribute("messaging.consumer_group.member_id", event.memberId);
if (event.reason) baseCtx.setAttribute("messaging.consumer_group.rebalance.reason", event.reason);
if (groupState.state) baseCtx.setAttribute("messaging.consumer_group.state", groupState.state);
const eventAttrs = {
"messaging.consumer_group.rebalance.type": event.type,
"messaging.consumer_group.rebalance.partition_count": event.partitions.length,
"messaging.consumer_group.rebalance.timestamp": event.timestamp,
...event.generation !== void 0 && { "messaging.consumer_group.generation": event.generation },
...event.memberId && { "messaging.consumer_group.member_id": event.memberId },
...event.reason && { "messaging.consumer_group.rebalance.reason": event.reason },
...groupState.state && { "messaging.consumer_group.state": groupState.state }
};
if (event.partitions.length <= 10) eventAttrs["messaging.consumer_group.rebalance.partitions"] = event.partitions.map((p) => `${p.topic}:${p.partition}`).join(",");
require_correlated_events.emitCorrelatedEvent(baseCtx, `consumer_group_${event.type}`, eventAttrs);
if (config.consumerGroupTracking?.onRebalance) config.consumerGroupTracking.onRebalance(consumerCtx, event);
if (event.type === "assigned" && config.consumerGroupTracking?.onPartitionsAssigned) config.consumerGroupTracking.onPartitionsAssigned(consumerCtx, event.partitions);
if (event.type === "revoked" && config.consumerGroupTracking?.onPartitionsRevoked) config.consumerGroupTracking.onPartitionsRevoked(consumerCtx, event.partitions);
},
recordHeartbeat(healthy, latencyMs) {
groupState.lastHeartbeat = Date.now();
baseCtx.setAttribute("messaging.consumer_group.heartbeat.healthy", healthy);
if (latencyMs !== void 0) baseCtx.setAttribute("messaging.consumer_group.heartbeat.latency_ms", latencyMs);
require_correlated_events.emitCorrelatedEvent(baseCtx, "consumer_group_heartbeat", {
"messaging.consumer_group.heartbeat.healthy": healthy,
"messaging.consumer_group.heartbeat.timestamp": groupState.lastHeartbeat,
...latencyMs !== void 0 && { "messaging.consumer_group.heartbeat.latency_ms": latencyMs }
});
},
recordPartitionLag(lag) {
const prefix = `messaging.consumer_group.lag.${lag.topic}.${lag.partition}`;
baseCtx.setAttribute(`${prefix}.current_offset`, lag.currentOffset);
baseCtx.setAttribute(`${prefix}.end_offset`, lag.endOffset);
baseCtx.setAttribute(`${prefix}.lag`, lag.lag);
require_correlated_events.emitCorrelatedEvent(baseCtx, "partition_lag_recorded", {
"messaging.consumer_group.lag.topic": lag.topic,
"messaging.consumer_group.lag.partition": lag.partition,
"messaging.consumer_group.lag.current_offset": lag.currentOffset,
"messaging.consumer_group.lag.end_offset": lag.endOffset,
"messaging.consumer_group.lag.lag": lag.lag,
"messaging.consumer_group.lag.timestamp": lag.timestamp
});
},
getConsumerGroupState() {
if (!config.consumerGroup) return null;
return {
groupId: config.consumerGroup,
memberId: groupState.memberId ?? void 0,
groupInstanceId: groupState.groupInstanceId ?? void 0,
assignedPartitions: [...groupState.assignedPartitions],
generation: groupState.generation ?? void 0,
isActive: groupState.isActive,
lastHeartbeat: groupState.lastHeartbeat ?? void 0,
state: groupState.state ?? void 0
};
},
getMemberId() {
return groupState.memberId;
},
getAssignedPartitions() {
return [...groupState.assignedPartitions];
}
};
return consumerCtx;
}
/**
* Set OTel semantic convention attributes for producer
*/
function setProducerAttributes(ctx, config) {
ctx.setAttribute("messaging.system", config.system);
ctx.setAttribute("messaging.operation", "publish");
ctx.setAttribute("messaging.destination.name", config.destination);
if (config.system === "kafka") ctx.setAttribute("messaging.kafka.destination.topic", config.destination);
if (config.attributes) setCustomAttributes(ctx, config.attributes);
}
/**
* Set dynamic producer attributes from arguments
*/
function setDynamicProducerAttributes(ctx, config, args) {
if (config.messageIdFrom) {
const messageId = extractValue(config.messageIdFrom, args);
if (messageId !== void 0) ctx.setAttribute("messaging.message.id", String(messageId));
}
if (config.partitionFrom) {
const partition = extractValue(config.partitionFrom, args);
if (partition !== void 0) ctx.setAttribute("messaging.kafka.destination.partition", Number(partition));
}
if (config.keyFrom) {
const key = extractValue(config.keyFrom, args);
if (key !== void 0) ctx.setAttribute("messaging.kafka.message.key", String(key));
}
}
/**
* Set OTel semantic convention attributes for consumer
*/
function setConsumerAttributes(ctx, config) {
ctx.setAttribute("messaging.system", config.system);
ctx.setAttribute("messaging.operation", config.batchMode ? "receive" : "process");
ctx.setAttribute("messaging.destination.name", config.destination);
if (config.consumerGroup) {
ctx.setAttribute("messaging.consumer.group", config.consumerGroup);
if (config.system === "kafka") ctx.setAttribute("messaging.kafka.consumer.group", config.consumerGroup);
}
if (config.system === "kafka") ctx.setAttribute("messaging.kafka.destination.topic", config.destination);
if (config.attributes) setCustomAttributes(ctx, config.attributes);
}
/**
* Extract links from message headers and add to span
*
* Uses W3C trace context by default, falls back to customContextExtractor if provided.
* Also populates linkStorage for getProducerLinks() and DLQ auto-linking.
*/
async function extractAndAddLinks(ctx, config, args, linkStorage) {
if (!config.headersFrom && !config.customContextExtractor) return;
const links = [];
if (config.batchMode && Array.isArray(args[0])) {
const messages = args[0];
if (config.headersFrom) {
const batchLinks = require_sampling.extractLinksFromBatch(messages.map((msg) => {
return { headers: extractHeaders(config.headersFrom, msg) };
}), "headers");
links.push(...batchLinks);
}
if (config.customContextExtractor && config.headersFrom) for (const msg of messages) {
const headers = extractHeaders(config.headersFrom, msg);
if (headers) {
if (!require_sampling.createLinkFromHeaders(headers)) {
const customContext = config.customContextExtractor(headers);
if (customContext) links.push({
context: customContext,
attributes: { "messaging.link.source": "custom_extractor" }
});
}
}
}
ctx.setAttribute("messaging.batch.message_count", messages.length);
} else {
const msg = args[0];
const headers = config.headersFrom ? extractHeaders(config.headersFrom, msg) : void 0;
if (headers) {
const w3cLink = require_sampling.createLinkFromHeaders(headers);
if (w3cLink) links.push(w3cLink);
else if (config.customContextExtractor) {
const customContext = config.customContextExtractor(headers);
if (customContext) links.push({
context: customContext,
attributes: { "messaging.link.source": "custom_extractor" }
});
}
}
}
if (links.length > 0) {
ctx.addLinks(links);
linkStorage.links.push(...links);
}
}
/**
* Extract lag metrics and set as span attributes
*/
async function extractLagMetrics(ctx, lagConfig, args) {
const msg = Array.isArray(args[0]) ? args[0][0] : args[0];
let currentOffset;
if (lagConfig.getCurrentOffset && msg) {
currentOffset = lagConfig.getCurrentOffset(msg);
if (currentOffset !== void 0) ctx.setAttribute("messaging.kafka.message.offset", currentOffset);
}
if (lagConfig.getPartition && msg) {
const partition = lagConfig.getPartition(msg);
if (partition !== void 0) ctx.setAttribute("messaging.kafka.partition", partition);
}
if (lagConfig.getEndOffset) try {
const endOffset = await Promise.resolve(lagConfig.getEndOffset());
if (endOffset !== void 0 && currentOffset !== void 0) {
const lag = endOffset - currentOffset;
ctx.setAttribute("messaging.kafka.consumer_lag", lag);
require_correlated_events.emitCorrelatedEvent(ctx, "consumer_lag_measured", {
"messaging.kafka.consumer_lag": lag,
"messaging.kafka.message.offset": currentOffset,
"messaging.kafka.high_watermark": endOffset
});
}
} catch {}
if (lagConfig.getCommittedOffset) try {
const committedOffset = await Promise.resolve(lagConfig.getCommittedOffset());
if (committedOffset !== void 0) ctx.setAttribute("messaging.kafka.committed_offset", committedOffset);
} catch {}
if (Array.isArray(args[0]) && args[0].length > 0) {
const messages = args[0];
if (lagConfig.getCurrentOffset) {
const firstOffset = lagConfig.getCurrentOffset(messages[0]);
const lastMessage = messages.at(-1);
const lastOffset = lastMessage === void 0 ? void 0 : lagConfig.getCurrentOffset(lastMessage);
if (firstOffset !== void 0) ctx.setAttribute("messaging.batch.first_offset", firstOffset);
if (lastOffset !== void 0) ctx.setAttribute("messaging.batch.last_offset", lastOffset);
}
}
}
/**
* Extract headers from message using config
*/
function extractHeaders(headersFrom, msg) {
if (typeof headersFrom === "function") return headersFrom(msg);
if (typeof msg === "object" && msg !== null) {
const value = msg[headersFrom];
if (typeof value === "object" && value !== null) return value;
}
}
/**
* Extract value from arguments using config
*/
function extractValue(extractor, args) {
if (typeof extractor === "function") return extractor(args);
const firstArg = args[0];
if (typeof firstArg === "object" && firstArg !== null) return firstArg[extractor];
}
/**
* Set custom attributes on context, handling non-primitive values
*/
function setCustomAttributes(ctx, attributes) {
for (const [key, value] of Object.entries(attributes)) if (value !== void 0 && value !== null) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") ctx.setAttribute(key, value);
else if (Array.isArray(value)) {
const cleanArray = value.filter((v) => v !== null && v !== void 0 && (typeof v === "string" || typeof v === "number" || typeof v === "boolean"));
if (cleanArray.length > 0) ctx.setAttribute(key, cleanArray);
} else ctx.setAttribute(key, JSON.stringify(value));
}
/**
* Extract and process ordering information from message
*
* Handles:
* - Sequence number extraction and tracking
* - Out-of-order detection
* - Duplicate detection
* - Span attribute setting
* - Callback invocation
*/
function extractAndProcessOrdering(ctx, config, args, orderingState) {
const ordering = config.ordering;
if (!ordering) return;
const messages = config.batchMode && Array.isArray(args[0]) ? args[0] : [args[0]];
if (messages.length === 0) return;
let outOfOrderCount = 0;
let duplicateCount = 0;
let lastSequence = null;
let lastPartitionKey = null;
let lastMessageId = null;
for (const [i, msg] of messages.entries()) {
if (!msg) continue;
let msgSequence = null;
let msgPartitionKey = null;
let msgId = null;
if (ordering.sequenceFrom) {
const seq = ordering.sequenceFrom(msg);
if (seq !== void 0) {
msgSequence = seq;
lastSequence = seq;
}
}
if (ordering.partitionKeyFrom) {
const key = ordering.partitionKeyFrom(msg);
if (key !== void 0) {
msgPartitionKey = key;
lastPartitionKey = key;
}
}
if (ordering.messageIdFrom) {
const id = ordering.messageIdFrom(msg);
if (id !== void 0) {
msgId = id;
lastMessageId = id;
}
}
if (ordering.detectOutOfOrder && msgSequence !== null) {
const trackerKey = buildTrackerKey(config, {
sequenceNumber: msgSequence,
partitionKey: msgPartitionKey,
messageId: msgId,
isDuplicate: false,
outOfOrderInfo: null
});
const prevSequence = sequenceTrackers.get(trackerKey);
if (prevSequence !== void 0) {
const expectedSequence = prevSequence + 1;
if (msgSequence !== expectedSequence) {
outOfOrderCount++;
const gap = msgSequence - expectedSequence;
const outOfOrderInfo = {
currentSequence: msgSequence,
expectedSequence,
partitionKey: msgPartitionKey ?? void 0,
gap
};
if (!orderingState.outOfOrderInfo) orderingState.outOfOrderInfo = outOfOrderInfo;
require_correlated_events.emitCorrelatedEvent(ctx, "message_out_of_order", {
"messaging.ordering.batch_index": i,
"messaging.ordering.current_sequence": msgSequence,
"messaging.ordering.expected_sequence": expectedSequence,
"messaging.ordering.gap": gap,
...msgPartitionKey && { "messaging.ordering.partition_key": msgPartitionKey }
});
if (ordering.onOutOfOrder) ordering.onOutOfOrder(ctx, outOfOrderInfo);
}
}
sequenceTrackers.set(trackerKey, msgSequence);
}
if (ordering.detectDuplicates && msgId !== null) {
const dedupKey = buildDedupKey(config, {
sequenceNumber: msgSequence,
partitionKey: msgPartitionKey,
messageId: msgId,
isDuplicate: false,
outOfOrderInfo: null
});
if (deduplicationWindow.has(dedupKey)) {
duplicateCount++;
require_correlated_events.emitCorrelatedEvent(ctx, "message_duplicate", {
"messaging.ordering.batch_index": i,
"messaging.message.id": msgId
});
if (ordering.onDuplicate) ordering.onDuplicate(ctx, msgId);
} else {
deduplicationWindow.set(dedupKey, Date.now());
trimDeduplicationWindow(ordering.deduplicationWindowSize ?? DEFAULT_DEDUP_WINDOW_SIZE);
}
}
}
orderingState.sequenceNumber = lastSequence;
orderingState.partitionKey = lastPartitionKey;
orderingState.messageId = lastMessageId;
orderingState.isDuplicate = duplicateCount > 0;
if (lastSequence !== null) ctx.setAttribute("messaging.message.sequence_number", lastSequence);
if (lastPartitionKey !== null) ctx.setAttribute("messaging.message.partition_key", lastPartitionKey);
if (lastMessageId !== null) ctx.setAttribute("messaging.message.id", lastMessageId);
if (outOfOrderCount > 0) {
ctx.setAttribute("messaging.ordering.out_of_order", true);
ctx.setAttribute("messaging.ordering.out_of_order_count", outOfOrderCount);
}
if (duplicateCount > 0) {
ctx.setAttribute("messaging.ordering.duplicate", true);
ctx.setAttribute("messaging.ordering.duplicate_count", duplicateCount);
}
}
/**
* Build a unique key for sequence tracking based on system, destination, and partition
*/
function buildTrackerKey(config, orderingState) {
const parts = [config.system, config.destination];
if (orderingState.partitionKey) parts.push(orderingState.partitionKey);
if (config.consumerGroup) parts.push(config.consumerGroup);
return parts.join(":");
}
/**
* Build a unique key for deduplication based on system, destination, and message ID
*/
function buildDedupKey(config, orderingState) {
const parts = [config.system, config.destination];
if (orderingState.messageId) parts.push(orderingState.messageId);
return parts.join(":");
}
/**
* Clear sequence tracking state (useful for testing)
*/
function clearOrderingState() {
sequenceTrackers.clear();
deduplicationWindow.clear();
}
//#endregion
exports.clearOrderingState = clearOrderingState;
exports.traceConsumer = traceConsumer;
exports.traceProducer = traceProducer;
//# sourceMappingURL=messaging.cjs.map