autotel
Version:
Write Once, Observe Anywhere
338 lines (336 loc) • 10.7 kB
JavaScript
import { trace } from './chunk-HPUGKUMZ.js';
import { getActiveSpan } from './chunk-B3ZHLLMP.js';
import { AsyncLocalStorage } from 'async_hooks';
var workflowStates = /* @__PURE__ */ new WeakMap();
var workflowContextStorage = new AsyncLocalStorage();
function traceWorkflow(config) {
const spanName = `workflow.${config.name}`;
return (fnFactory) => {
return trace(spanName, (baseCtx) => {
return async (...args) => {
const workflowId = typeof config.workflowId === "function" ? config.workflowId(...args) : config.workflowId;
const ctx = createWorkflowContext(baseCtx, config.name, workflowId);
ctx.setAttribute("workflow.name", config.name);
ctx.setAttribute("workflow.id", workflowId);
if (config.version) {
ctx.setAttribute("workflow.version", config.version);
}
ctx.setAttribute("workflow.status", "running");
if (config.attributes) {
for (const [key, value] of Object.entries(config.attributes)) {
if (value !== void 0) {
ctx.setAttribute(key, value);
}
}
}
return workflowContextStorage.run(ctx, async () => {
try {
const userFn = fnFactory(ctx);
const result = await userFn(...args);
ctx.setWorkflowStatus("completed");
config.onComplete?.(ctx, result);
return result;
} catch (error) {
ctx.setWorkflowStatus("failed");
config.onFailed?.(ctx, error);
const state = getWorkflowState();
if (state && state.compensations.size > 0) {
ctx.setWorkflowStatus("compensating");
config.onCompensating?.(ctx);
try {
await ctx.compensate(error);
ctx.setWorkflowStatus("compensated");
} catch (compensationError) {
ctx.setWorkflowStatus("compensation_failed");
ctx.setAttribute(
"workflow.compensation.error",
String(compensationError)
);
}
}
throw error;
}
});
};
});
};
}
function traceStep(config) {
return (fn) => {
const spanName = `step.${config.name}`;
return trace(spanName, (baseCtx) => {
return async (...args) => {
const workflowCtx = workflowContextStorage.getStore() ?? null;
const ctx = createStepContext(baseCtx, config, workflowCtx);
ctx.setAttribute("workflow.step.name", config.name);
ctx.setAttribute("workflow.step.index", ctx.getStepIndex());
ctx.setAttribute("workflow.step.status", "running");
if (config.description) {
ctx.setAttribute("workflow.step.description", config.description);
}
if (config.idempotent) {
ctx.setAttribute("workflow.step.idempotent", true);
}
if (config.attributes) {
for (const [key, value] of Object.entries(config.attributes)) {
if (value !== void 0) {
ctx.setAttribute(key, value);
}
}
}
await addStepLinks(ctx, config, workflowCtx);
if (config.compensate && workflowCtx) {
workflowCtx.registerCompensation(config.name, config.compensate);
}
let lastError;
const maxAttempts = config.retry?.maxAttempts ?? 1;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
if (attempt > 1) {
ctx.setAttribute("workflow.step.retry_attempt", attempt);
ctx.addEvent("step_retry", {
"workflow.step.attempt": attempt,
"workflow.step.max_attempts": maxAttempts
});
if (config.retry?.backoffMs) {
await sleep(config.retry.backoffMs * attempt);
}
}
const result = await fn(...args);
ctx.setAttribute("workflow.step.status", "completed");
ctx.complete();
config.onComplete?.(ctx);
return result;
} catch (error) {
lastError = error;
if (attempt < maxAttempts) {
ctx.addEvent("step_retry_scheduled", {
"workflow.step.error": String(error),
"workflow.step.attempt": attempt
});
}
}
}
ctx.setAttribute("workflow.step.status", "failed");
ctx.setAttribute("workflow.step.error", String(lastError));
config.onFailed?.(ctx, lastError);
throw lastError;
};
});
};
}
function createWorkflowContext(baseCtx, workflowName, workflowId) {
const span = getActiveSpan();
const state = {
workflowId,
workflowName,
status: "running",
steps: /* @__PURE__ */ new Map(),
stepCounter: 0,
compensations: /* @__PURE__ */ new Map()
};
if (span) {
workflowStates.set(span, state);
}
return {
...baseCtx,
getWorkflowId() {
return workflowId;
},
getWorkflowName() {
return workflowName;
},
getStatus() {
return state.status;
},
completeStep(stepName) {
let step = state.steps.get(stepName);
if (!step) {
step = {
name: stepName,
index: state.stepCounter++,
status: "pending",
startTime: Date.now()
};
state.steps.set(stepName, step);
}
step.status = "completed";
step.endTime = Date.now();
const currentSpan = getActiveSpan();
if (currentSpan) {
step.spanContext = currentSpan.spanContext();
}
},
getPreviousStep(stepName) {
if (stepName) {
const step = state.steps.get(stepName);
return step?.spanContext ?? null;
}
let lastStep = null;
for (const step of state.steps.values()) {
if (step.status === "completed" && (!lastStep || step.index > lastStep.index)) {
lastStep = step;
}
}
return lastStep?.spanContext ?? null;
},
getCompletedSteps() {
const completed = [];
for (const [name, step] of state.steps) {
if (step.status === "completed") {
completed.push(name);
}
}
return completed.toSorted(
(a, b) => (state.steps.get(a)?.index ?? 0) - (state.steps.get(b)?.index ?? 0)
);
},
registerCompensation(stepName, handler) {
state.compensations.set(stepName, handler);
},
async compensate(error) {
const compensationOrder = [...state.compensations.entries()].toReversed();
for (const [stepName, handler] of compensationOrder) {
const step = state.steps.get(stepName);
if (step && step.status === "completed") {
try {
baseCtx.addEvent("compensation_started", {
"workflow.step.name": stepName
});
await Promise.resolve(handler(error));
this.recordCompensation(stepName, true);
step.status = "compensated";
} catch (compensationError) {
this.recordCompensation(
stepName,
false,
compensationError
);
throw compensationError;
}
}
}
},
recordCompensation(stepName, success, error) {
baseCtx.addEvent("compensation_completed", {
"workflow.step.name": stepName,
"workflow.compensation.success": success,
...error && { "workflow.compensation.error": String(error) }
});
baseCtx.setAttribute(
`workflow.compensation.${stepName}`,
success ? "success" : "failed"
);
},
setWorkflowStatus(status) {
state.status = status;
baseCtx.setAttribute("workflow.status", status);
baseCtx.addEvent("workflow_status_changed", {
"workflow.status": status
});
}
};
}
function createStepContext(baseCtx, config, workflowCtx) {
let stepIndex = config.index ?? 0;
if (workflowCtx) {
const span = getActiveSpan();
if (span) {
const state = workflowStates.get(span);
if (state) {
stepIndex = config.index ?? state.stepCounter++;
}
}
}
if (workflowCtx) {
const wfSpan = getActiveSpan();
if (wfSpan) {
const state = workflowStates.get(wfSpan);
if (state) {
state.steps.set(config.name, {
name: config.name,
index: stepIndex,
status: "running",
startTime: Date.now(),
compensate: config.compensate
});
}
}
}
return {
...baseCtx,
getStepName() {
return config.name;
},
getStepIndex() {
return stepIndex;
},
complete() {
if (workflowCtx) {
workflowCtx.completeStep(config.name);
}
},
skip(reason) {
baseCtx.setAttribute("workflow.step.status", "skipped");
if (reason) {
baseCtx.setAttribute("workflow.step.skip_reason", reason);
}
baseCtx.addEvent("step_skipped", {
"workflow.step.name": config.name,
...reason && { "workflow.step.skip_reason": reason }
});
},
getWorkflowContext() {
return workflowCtx;
}
};
}
function getWorkflowState() {
const span = getActiveSpan();
return span ? workflowStates.get(span) : null;
}
async function addStepLinks(ctx, config, workflowCtx) {
if (!workflowCtx) return;
const links = [];
if (config.linkToPrevious) {
const prevSpanContext = workflowCtx.getPreviousStep();
if (prevSpanContext) {
links.push({
context: prevSpanContext,
attributes: {
"workflow.link.type": "sequence"
}
});
}
}
if (config.linkTo) {
const stepNames = Array.isArray(config.linkTo) ? config.linkTo : [config.linkTo];
for (const stepName of stepNames) {
const spanContext = workflowCtx.getPreviousStep(stepName);
if (spanContext) {
links.push({
context: spanContext,
attributes: {
"workflow.link.type": "dependency",
"workflow.link.step": stepName
}
});
}
}
}
if (links.length > 0) {
ctx.addLinks(links);
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getCurrentWorkflowContext() {
return workflowContextStorage.getStore() ?? null;
}
function isInWorkflow() {
return workflowContextStorage.getStore() !== void 0;
}
export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow };
//# sourceMappingURL=chunk-YN7USLHW.js.map
//# sourceMappingURL=chunk-YN7USLHW.js.map