UNPKG

autotel

Version:
525 lines (523 loc) 18.3 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_functional = require('./functional-C8B0Qa7o.cjs'); const require_correlated_events = require('./correlated-events-kSwLo3mi.cjs'); const require_business_baggage = require('./business-baggage.cjs'); let _opentelemetry_api = require("@opentelemetry/api"); //#region src/workflow-distributed.ts /** * Distributed workflow tracing with cross-service correlation * * Enables tracking workflows that span multiple microservices by propagating * workflow identity (workflowId, stepName, stepIndex) via baggage in message headers. * * Unlike local workflow.ts (which uses AsyncLocalStorage), distributed workflows * propagate context across network boundaries using W3C baggage. * * @example Order fulfillment saga across services * ```typescript * // Service A: Order Service * import { traceDistributedWorkflow, WorkflowBaggage } from 'autotel/workflow-distributed'; * import { traceProducer } from 'autotel/messaging'; * * export const createOrder = traceDistributedWorkflow({ * name: 'OrderFulfillment', * workflowIdFrom: (order) => order.id, * version: '1.0.0', * })(ctx => async (order: Order) => { * // Workflow baggage is auto-set * await publishToInventory(order); * }); * * const publishToInventory = traceProducer({ * system: 'kafka', * destination: 'inventory-requests', * propagateBaggage: true, // Includes workflow.* baggage * })(ctx => async (order) => { * await producer.send({ topic: 'inventory-requests', value: order }); * }); * * // Service B: Inventory Service * import { traceDistributedStep, WorkflowBaggage } from 'autotel/workflow-distributed'; * * export const processInventory = traceDistributedStep({ * name: 'ReserveInventory', * extractBaggage: true, // Extracts workflow.* from headers * })(ctx => async (message) => { * const workflow = WorkflowBaggage.get(ctx); * // workflow.workflowId === order.id (propagated from Service A) * console.log(`Processing step for workflow ${workflow.workflowId}`); * await reserveItems(message.items); * }); * ``` * * @module */ /** * Workflow baggage field definitions */ const workflowBaggageFields = { /** Unique identifier for the workflow instance */ workflowId: { type: "string", maxLength: 128, required: true }, /** Name/type of the workflow (e.g., "OrderFulfillment") */ workflowName: { type: "string", maxLength: 64, required: true }, /** Version of the workflow definition */ workflowVersion: { type: "string", maxLength: 32 }, /** Current step name */ stepName: { type: "string", maxLength: 64 }, /** Current step index (0-based) */ stepIndex: { type: "number" }, /** Total number of steps (if known) */ totalSteps: { type: "number" }, /** Parent workflow ID (for sub-workflows) */ parentWorkflowId: { type: "string", maxLength: 128 }, /** Correlation ID for external systems */ correlationId: { type: "string", maxLength: 128 }, /** Workflow priority */ priority: { type: "enum", values: [ "low", "normal", "high", "critical" ] }, /** Initiating user/system */ initiatedBy: { type: "string", maxLength: 64 }, /** Workflow start timestamp (ISO) */ startedAt: { type: "string", maxLength: 30 } }; /** * Pre-built baggage schema for distributed workflows * * Use this to read/write workflow context that propagates across services. * * @example Setting workflow baggage * ```typescript * WorkflowBaggage.set(ctx, { * workflowId: 'order-12345', * workflowName: 'OrderFulfillment', * stepName: 'ReserveInventory', * stepIndex: 1, * }); * ``` * * @example Reading workflow baggage in downstream service * ```typescript * const { workflowId, workflowName, stepIndex } = WorkflowBaggage.get(ctx); * console.log(`Processing ${workflowName} step ${stepIndex}`); * ``` */ const WorkflowBaggage = require_business_baggage.createSafeBaggageSchema(workflowBaggageFields, { prefix: "workflow", hashHighCardinality: false, redactPII: false }); /** * Create a traced distributed workflow function * * Wraps a function as the entry point for a distributed workflow. Automatically: * - Generates or extracts workflow ID * - Sets workflow baggage for downstream propagation * - Creates root span with workflow attributes * * @param config - Workflow configuration * @returns Factory function for the workflow handler * * @example Basic usage * ```typescript * export const createOrder = traceDistributedWorkflow({ * name: 'OrderFulfillment', * workflowIdFrom: (order) => order.id, * version: '1.0.0', * })(ctx => async (order: Order) => { * ctx.recordStepProgress('ValidateOrder', 0); * await validateOrder(order); * * ctx.recordStepProgress('ReserveInventory', 1); * await publishToInventoryService(order); * * return { workflowId: ctx.workflowId, status: 'started' }; * }); * ``` */ function traceDistributedWorkflow(config) { const spanName = `workflow.${config.name}`; return (fnFactory) => { return require_functional.trace({ name: spanName, spanKind: _opentelemetry_api.SpanKind.INTERNAL }, (baseCtx) => { return async (...args) => { const workflowId = config.workflowIdFrom(...args); const startedAt = (/* @__PURE__ */ new Date()).toISOString(); const baggageValues = { workflowId, workflowName: config.name, workflowVersion: config.version, stepIndex: 0, totalSteps: config.totalSteps, parentWorkflowId: config.parentWorkflowId, correlationId: config.correlationId, priority: config.priority, initiatedBy: config.initiatedBy, startedAt }; WorkflowBaggage.set(baseCtx, baggageValues); baseCtx.setAttribute("workflow.id", workflowId); baseCtx.setAttribute("workflow.name", config.name); if (config.version) baseCtx.setAttribute("workflow.version", config.version); if (config.totalSteps) baseCtx.setAttribute("workflow.total_steps", config.totalSteps); if (config.parentWorkflowId) baseCtx.setAttribute("workflow.parent_id", config.parentWorkflowId); if (config.priority) baseCtx.setAttribute("workflow.priority", config.priority); if (config.initiatedBy) baseCtx.setAttribute("workflow.initiated_by", config.initiatedBy); baseCtx.setAttribute("workflow.started_at", startedAt); if (config.attributes) for (const [key, value] of Object.entries(config.attributes)) baseCtx.setAttribute(key, value); const workflowCtx = { ...baseCtx, workflowId, workflowName: config.name, workflowVersion: config.version, getWorkflowBaggage() { return { ...baggageValues }; }, setWorkflowBaggage(values) { Object.assign(baggageValues, values); WorkflowBaggage.set(baseCtx, baggageValues); }, getWorkflowHeaders() { const headers = {}; const ctx = _opentelemetry_api.context.active(); _opentelemetry_api.propagation.inject(ctx, headers); return headers; }, recordStepProgress(stepName, stepIndex) { baggageValues.stepName = stepName; baggageValues.stepIndex = stepIndex; WorkflowBaggage.set(baseCtx, baggageValues); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.step_progress", { "workflow.step.name": stepName, "workflow.step.index": stepIndex }); } }; config.onStart?.(workflowCtx); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.started", { "workflow.id": workflowId, "workflow.name": config.name }); try { const result = await fnFactory(workflowCtx)(...args); config.onComplete?.(workflowCtx, result); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.completed", { "workflow.id": workflowId }); return result; } catch (error) { config.onError?.(workflowCtx, error); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.failed", { "workflow.id": workflowId, "workflow.error": error.message }); throw error; } }; }); }; } /** * Create a traced distributed workflow step * * Use in downstream services to trace steps that are part of a distributed workflow. * Automatically extracts workflow baggage from the current context. * * @param config - Step configuration * @returns Factory function for the step handler * * @example Consumer in downstream service * ```typescript * export const processInventory = traceConsumer({ * system: 'kafka', * destination: 'inventory-requests', * extractBaggage: true, // Extracts workflow.* from headers * })(ctx => { * // Wrap inner logic with traceDistributedStep * return traceDistributedStep({ * name: 'ReserveInventory', * })(stepCtx => async (message) => { * console.log(`Processing workflow ${stepCtx.workflowId}`); * await reserveItems(message.items); * })(message); * }); * ``` * * @example Standalone step handler * ```typescript * export const reserveInventory = traceDistributedStep({ * name: 'ReserveInventory', * idempotent: true, * })(ctx => async (request: InventoryRequest) => { * if (ctx.workflowId) { * console.log(`Part of workflow ${ctx.workflowId}, step ${ctx.stepIndex}`); * } * return await inventoryService.reserve(request.items); * }); * ``` */ function traceDistributedStep(config) { const spanName = `workflow.step.${config.name}`; return (fnFactory) => { return require_functional.trace({ name: spanName, spanKind: _opentelemetry_api.SpanKind.INTERNAL }, (baseCtx) => { return async (...args) => { let baggageValues = null; const extractBaggage = config.extractBaggage ?? true; if (typeof extractBaggage === "function") baggageValues = extractBaggage(args); else if (extractBaggage) { const extracted = WorkflowBaggage.get(baseCtx); if (extracted.workflowId && extracted.workflowName) baggageValues = extracted; } let stepIndex; if (config.stepIndex !== void 0) stepIndex = config.stepIndex; else if (baggageValues?.stepIndex === void 0) stepIndex = null; else stepIndex = baggageValues.stepIndex + 1; if (baggageValues) { baggageValues.stepName = config.name; if (stepIndex !== null) baggageValues.stepIndex = stepIndex; WorkflowBaggage.set(baseCtx, baggageValues); } baseCtx.setAttribute("workflow.step.name", config.name); if (stepIndex !== null) baseCtx.setAttribute("workflow.step.index", stepIndex); if (config.idempotent !== void 0) baseCtx.setAttribute("workflow.step.idempotent", config.idempotent); if (config.isCompensation) baseCtx.setAttribute("workflow.step.is_compensation", true); if (baggageValues) { baseCtx.setAttribute("workflow.id", baggageValues.workflowId); baseCtx.setAttribute("workflow.name", baggageValues.workflowName); if (baggageValues.workflowVersion) baseCtx.setAttribute("workflow.version", baggageValues.workflowVersion); if (baggageValues.totalSteps) baseCtx.setAttribute("workflow.total_steps", baggageValues.totalSteps); } if (config.attributes) for (const [key, value] of Object.entries(config.attributes)) baseCtx.setAttribute(key, value); let compensationData; const stepCtx = { ...baseCtx, workflowId: baggageValues?.workflowId ?? null, workflowName: baggageValues?.workflowName ?? null, stepName: config.name, stepIndex, isCompensation: config.isCompensation ?? false, getWorkflowBaggage() { return baggageValues ? { ...baggageValues } : null; }, updateWorkflowBaggage(values) { if (baggageValues) { Object.assign(baggageValues, values); WorkflowBaggage.set(baseCtx, baggageValues); } }, getWorkflowHeaders() { const headers = {}; const ctx = _opentelemetry_api.context.active(); _opentelemetry_api.propagation.inject(ctx, headers); return headers; }, requiresCompensation(data) { compensationData = data; baseCtx.setAttribute("workflow.step.requires_compensation", true); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.step.compensation_registered", { "workflow.step.name": config.name, ...data && { "workflow.step.compensation_data": JSON.stringify(data) } }); } }; config.onStart?.(stepCtx); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.step.started", { "workflow.step.name": config.name, ...baggageValues && { "workflow.id": baggageValues.workflowId } }); try { const result = await fnFactory(stepCtx)(...args); config.onComplete?.(stepCtx, result); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.step.completed", { "workflow.step.name": config.name }); return result; } catch (error) { config.onError?.(stepCtx, error); require_correlated_events.emitCorrelatedEvent(baseCtx, "workflow.step.failed", { "workflow.step.name": config.name, "workflow.step.error": error.message, ...compensationData && { "workflow.step.requires_compensation": true } }); throw error; } }; }); }; } /** * Generate a unique workflow ID * * @param prefix - Optional prefix for the ID * @returns A unique workflow ID * * @example * ```typescript * const workflowId = generateWorkflowId('order'); // "order-abc123def456" * ``` */ function generateWorkflowId(prefix) { const random = Math.random().toString(36).slice(2, 15); const id = `${Date.now().toString(36)}-${random}`; return prefix ? `${prefix}-${id}` : id; } /** * Check if the current context is part of a distributed workflow * * @param ctx - The trace context * @returns True if workflow baggage is present */ function isInDistributedWorkflow(ctx) { const baggage = WorkflowBaggage.get(ctx); return !!(baggage.workflowId && baggage.workflowName); } /** * Get workflow progress information * * @param ctx - The trace context * @returns Progress info or null if not in a workflow */ function getWorkflowProgress(ctx) { const baggage = WorkflowBaggage.get(ctx); if (!baggage.workflowId || !baggage.workflowName) return null; const percentComplete = baggage.totalSteps && baggage.stepIndex !== void 0 ? Math.round((baggage.stepIndex + 1) / baggage.totalSteps * 100) : null; return { workflowId: baggage.workflowId, workflowName: baggage.workflowName, currentStep: baggage.stepName ?? null, currentStepIndex: baggage.stepIndex ?? null, totalSteps: baggage.totalSteps ?? null, percentComplete }; } /** * Create workflow correlation headers for manual propagation * * Use when you need to manually add workflow context to outgoing requests. * * @param values - Workflow baggage values * @returns Headers object with workflow baggage * * @example * ```typescript * const headers = createWorkflowHeaders({ * workflowId: 'order-123', * workflowName: 'OrderFulfillment', * stepIndex: 2, * }); * * await fetch('/api/inventory', { headers }); * ``` */ function createWorkflowHeaders(values) { const headers = {}; const baggageEntries = []; if (values.workflowId) baggageEntries.push(`workflow.workflowId=${encodeURIComponent(values.workflowId)}`); if (values.workflowName) baggageEntries.push(`workflow.workflowName=${encodeURIComponent(values.workflowName)}`); if (values.workflowVersion) baggageEntries.push(`workflow.workflowVersion=${encodeURIComponent(values.workflowVersion)}`); if (values.stepName) baggageEntries.push(`workflow.stepName=${encodeURIComponent(values.stepName)}`); if (values.stepIndex !== void 0) baggageEntries.push(`workflow.stepIndex=${values.stepIndex}`); if (values.totalSteps !== void 0) baggageEntries.push(`workflow.totalSteps=${values.totalSteps}`); if (values.priority) baggageEntries.push(`workflow.priority=${values.priority}`); if (values.correlationId) baggageEntries.push(`workflow.correlationId=${encodeURIComponent(values.correlationId)}`); if (values.parentWorkflowId) baggageEntries.push(`workflow.parentWorkflowId=${encodeURIComponent(values.parentWorkflowId)}`); if (values.initiatedBy) baggageEntries.push(`workflow.initiatedBy=${encodeURIComponent(values.initiatedBy)}`); if (values.startedAt) baggageEntries.push(`workflow.startedAt=${encodeURIComponent(values.startedAt)}`); if (baggageEntries.length > 0) headers["baggage"] = baggageEntries.join(","); return headers; } /** * Parse workflow context from baggage header * * @param baggageHeader - The baggage header value * @returns Parsed workflow values or null */ function parseWorkflowFromBaggage(baggageHeader) { if (!baggageHeader) return null; const values = {}; const entries = baggageHeader.split(","); for (const entry of entries) { const [key, value] = entry.trim().split("="); if (!key || !value) continue; const decodedValue = decodeURIComponent(value); switch (key) { case "workflow.workflowId": values.workflowId = decodedValue; break; case "workflow.workflowName": values.workflowName = decodedValue; break; case "workflow.workflowVersion": values.workflowVersion = decodedValue; break; case "workflow.stepName": values.stepName = decodedValue; break; case "workflow.stepIndex": values.stepIndex = Number.parseInt(decodedValue, 10); break; case "workflow.totalSteps": values.totalSteps = Number.parseInt(decodedValue, 10); break; case "workflow.priority": values.priority = decodedValue; break; case "workflow.correlationId": values.correlationId = decodedValue; break; case "workflow.parentWorkflowId": values.parentWorkflowId = decodedValue; break; case "workflow.initiatedBy": values.initiatedBy = decodedValue; break; case "workflow.startedAt": values.startedAt = decodedValue; break; } } return Object.keys(values).length > 0 ? values : null; } //#endregion exports.WorkflowBaggage = WorkflowBaggage; exports.createWorkflowHeaders = createWorkflowHeaders; exports.generateWorkflowId = generateWorkflowId; exports.getWorkflowProgress = getWorkflowProgress; exports.isInDistributedWorkflow = isInDistributedWorkflow; exports.parseWorkflowFromBaggage = parseWorkflowFromBaggage; exports.traceDistributedStep = traceDistributedStep; exports.traceDistributedWorkflow = traceDistributedWorkflow; //# sourceMappingURL=workflow-distributed.cjs.map