@kb-labs/workflow-daemon 2.88.0 → 2.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +250 -125
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
package/dist/index.js
CHANGED
|
@@ -25,7 +25,8 @@ async function createWorkflowWorker(options) {
|
|
|
25
25
|
workspaceRoot,
|
|
26
26
|
concurrency = 5,
|
|
27
27
|
defaultTimeout = 12e4,
|
|
28
|
-
analytics
|
|
28
|
+
analytics,
|
|
29
|
+
debugMode = process.env["WORKFLOW_DEBUG"] === "true"
|
|
29
30
|
} = options;
|
|
30
31
|
let isRunning = false;
|
|
31
32
|
let stopRequested = false;
|
|
@@ -33,8 +34,9 @@ async function createWorkflowWorker(options) {
|
|
|
33
34
|
const claimedJobs = /* @__PURE__ */ new Set();
|
|
34
35
|
const executionBackend = platform2.executionBackend;
|
|
35
36
|
const runner = new SandboxRunner({
|
|
37
|
+
// IExecutionBackend<unknown> (core-contracts) is structurally compatible with
|
|
38
|
+
// ExecutionBackend (plugin-execution) — both expose the same execute/health/stats/shutdown API.
|
|
36
39
|
backend: executionBackend,
|
|
37
|
-
// ExecutionBackend type from plugin-execution
|
|
38
40
|
cliApi,
|
|
39
41
|
workspaceRoot,
|
|
40
42
|
defaultTimeout
|
|
@@ -166,6 +168,10 @@ async function createWorkflowWorker(options) {
|
|
|
166
168
|
exprCtx.steps[s.spec.id] = {
|
|
167
169
|
outputs: s.outputs ?? {}
|
|
168
170
|
};
|
|
171
|
+
} else if (s.status === "failed" && s.spec.uses === "builtin:approval" && s.spec.id && s.outputs) {
|
|
172
|
+
exprCtx.steps[s.spec.id] = {
|
|
173
|
+
outputs: s.outputs
|
|
174
|
+
};
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
}
|
|
@@ -185,7 +191,29 @@ async function createWorkflowWorker(options) {
|
|
|
185
191
|
continue;
|
|
186
192
|
}
|
|
187
193
|
}
|
|
194
|
+
if (debugMode) {
|
|
195
|
+
jobLogger.info("[debug] Expression context for step", {
|
|
196
|
+
runId: run.id,
|
|
197
|
+
jobId: job.id,
|
|
198
|
+
stepId: step.id,
|
|
199
|
+
stepUses: step.spec.uses,
|
|
200
|
+
exprCtx: {
|
|
201
|
+
inputs: exprCtx.inputs,
|
|
202
|
+
steps: exprCtx.steps,
|
|
203
|
+
env: exprCtx.env
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
188
207
|
const interpolatedWith = step.spec.with ? interpolateObject(step.spec.with, exprCtx) : void 0;
|
|
208
|
+
if (debugMode && step.spec.with) {
|
|
209
|
+
jobLogger.info("[debug] Step input interpolation", {
|
|
210
|
+
runId: run.id,
|
|
211
|
+
jobId: job.id,
|
|
212
|
+
stepId: step.id,
|
|
213
|
+
rawWith: step.spec.with,
|
|
214
|
+
resolvedWith: interpolatedWith
|
|
215
|
+
});
|
|
216
|
+
}
|
|
189
217
|
const stepExecutionId = `wf-${run.id}-${job.id}-${step.id}-${Date.now()}`;
|
|
190
218
|
const stepLogger = jobLogger.child({
|
|
191
219
|
operation: "workflow.step",
|
|
@@ -195,20 +223,31 @@ async function createWorkflowWorker(options) {
|
|
|
195
223
|
spanId: stepExecutionId,
|
|
196
224
|
invocationId: stepExecutionId
|
|
197
225
|
});
|
|
226
|
+
if (interpolatedWith) {
|
|
227
|
+
const stateStore = engine.getStateStore();
|
|
228
|
+
await stateStore.updateStep(run.id, job.id, step.id, (draft) => {
|
|
229
|
+
draft.resolvedInputs = interpolatedWith;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
198
232
|
stepLogger.info("Executing step", {
|
|
199
233
|
runId: run.id,
|
|
200
234
|
jobId: job.id,
|
|
201
235
|
stepId: step.id,
|
|
202
|
-
uses: step.spec.uses
|
|
236
|
+
uses: step.spec.uses,
|
|
237
|
+
// Log resolved inputs so failures are debuggable without a debug flag.
|
|
238
|
+
// Sensitive values (tokens, secrets) will appear here — acceptable for
|
|
239
|
+
// server-side structured logs; do not surface in public UI.
|
|
240
|
+
inputs: interpolatedWith
|
|
203
241
|
});
|
|
204
242
|
if (step.spec.uses === "builtin:approval") {
|
|
243
|
+
if (step.status === "failed") {
|
|
244
|
+
stepLogger.info("Approval already rejected \u2014 skipping to gate", {
|
|
245
|
+
runId: run.id,
|
|
246
|
+
stepId: step.id
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
205
250
|
if (step.status !== "waiting_approval") {
|
|
206
|
-
if (interpolatedWith) {
|
|
207
|
-
const stateStore = engine.getStateStore();
|
|
208
|
-
await stateStore.updateStep(run.id, job.id, step.id, (draft) => {
|
|
209
|
-
draft.spec = { ...draft.spec, with: interpolatedWith };
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
251
|
await engine.markStepWaitingApproval(run.id, job.id, step.id);
|
|
213
252
|
}
|
|
214
253
|
stepLogger.info("Waiting for approval", {
|
|
@@ -227,9 +266,8 @@ async function createWorkflowWorker(options) {
|
|
|
227
266
|
break;
|
|
228
267
|
}
|
|
229
268
|
if (currentStep.status === "failed") {
|
|
230
|
-
const rejectMsg = currentStep.error?.message ?? "Approval rejected";
|
|
231
269
|
stepLogger.info("Approval rejected", { runId: run.id, stepId: step.id });
|
|
232
|
-
|
|
270
|
+
break;
|
|
233
271
|
}
|
|
234
272
|
}
|
|
235
273
|
if (stopRequested) {
|
|
@@ -388,12 +426,21 @@ async function createWorkflowWorker(options) {
|
|
|
388
426
|
});
|
|
389
427
|
throw error;
|
|
390
428
|
}
|
|
391
|
-
|
|
429
|
+
const stepOutputs = result.status === "success" ? result.outputs : void 0;
|
|
430
|
+
await engine.markStepCompleted(run.id, job.id, step.id, stepOutputs);
|
|
392
431
|
stepLogger.info("Step completed", {
|
|
393
432
|
runId: run.id,
|
|
394
433
|
jobId: job.id,
|
|
395
434
|
stepId: step.id
|
|
396
435
|
});
|
|
436
|
+
if (debugMode && stepOutputs) {
|
|
437
|
+
stepLogger.info("[debug] Step outputs", {
|
|
438
|
+
runId: run.id,
|
|
439
|
+
jobId: job.id,
|
|
440
|
+
stepId: step.id,
|
|
441
|
+
outputs: stepOutputs
|
|
442
|
+
});
|
|
443
|
+
}
|
|
397
444
|
}
|
|
398
445
|
await engine.markJobCompleted(run.id, job.id);
|
|
399
446
|
const jobDuration = Date.now() - jobStartTime;
|
|
@@ -631,12 +678,25 @@ var JobBroker = class {
|
|
|
631
678
|
await this.engine.cancelRun(runId);
|
|
632
679
|
this.logger.info("Job cancelled", { runId });
|
|
633
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Query logs for a specific run, with optional step-level filtering.
|
|
683
|
+
* Used by GET /api/v1/runs/:runId/logs?stepId=...
|
|
684
|
+
*/
|
|
685
|
+
async getRunLogs(runId, options) {
|
|
686
|
+
return this._queryLogs(runId, options);
|
|
687
|
+
}
|
|
634
688
|
/**
|
|
635
689
|
* Get job logs by run ID.
|
|
636
690
|
* Returns execution logs with optional filtering by level and pagination.
|
|
637
691
|
* Uses platform.logs service to query logs by runId metadata.
|
|
638
692
|
*/
|
|
639
693
|
async getJobLogs(runId, options) {
|
|
694
|
+
return this._queryLogs(runId, options);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Internal: query + filter logs for a given run (optionally narrow to one step).
|
|
698
|
+
*/
|
|
699
|
+
async _queryLogs(runId, options) {
|
|
640
700
|
const run = await this.engine.getRun(runId);
|
|
641
701
|
if (!run) {
|
|
642
702
|
return [];
|
|
@@ -652,18 +712,22 @@ var JobBroker = class {
|
|
|
652
712
|
level: options?.level && options.level !== "all" ? options.level : void 0
|
|
653
713
|
},
|
|
654
714
|
{
|
|
655
|
-
limit:
|
|
656
|
-
// Query more logs, then filter in-memory
|
|
715
|
+
limit: 2e3,
|
|
657
716
|
offset: 0
|
|
658
717
|
}
|
|
659
718
|
);
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
719
|
+
const filtered = queryResult.logs.filter((log) => {
|
|
720
|
+
if (log.fields["runId"] !== runId) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
if (options?.stepId && log.fields["stepId"] !== options.stepId) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
return true;
|
|
663
727
|
});
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
return
|
|
728
|
+
filtered.sort((a, b) => a.timestamp - b.timestamp);
|
|
729
|
+
const page = filtered.slice(offset, offset + limit);
|
|
730
|
+
return page.map((log) => ({
|
|
667
731
|
timestamp: new Date(log.timestamp).toISOString(),
|
|
668
732
|
level: log.level,
|
|
669
733
|
message: log.message,
|
|
@@ -1284,6 +1348,10 @@ var WorkflowHostService = class {
|
|
|
1284
1348
|
const logs = await this.options.jobBroker.getJobLogs(jobId, options);
|
|
1285
1349
|
return logs;
|
|
1286
1350
|
}
|
|
1351
|
+
async getRunLogs(runId, options) {
|
|
1352
|
+
const logs = await this.options.jobBroker.getRunLogs(runId, options);
|
|
1353
|
+
return logs;
|
|
1354
|
+
}
|
|
1287
1355
|
async cancelJob(tenantId, jobId) {
|
|
1288
1356
|
await this.options.engine.cancelRun(jobId);
|
|
1289
1357
|
this.options.logger.info("Job cancelled", { jobId, tenantId });
|
|
@@ -1360,7 +1428,7 @@ var WorkflowHostService = class {
|
|
|
1360
1428
|
};
|
|
1361
1429
|
const triggerType = request.trigger?.type === "cron" ? "schedule" : request.trigger?.type === "api" ? "webhook" : "manual";
|
|
1362
1430
|
const specInputDefs = specInput["inputs"] ?? {};
|
|
1363
|
-
const userInputs = request.input && typeof request.input === "object" ? request.input : {};
|
|
1431
|
+
const userInputs = request.inputs ?? (request.input && typeof request.input === "object" ? request.input : {});
|
|
1364
1432
|
const resolvedInputs = {};
|
|
1365
1433
|
for (const [key, def] of Object.entries(specInputDefs)) {
|
|
1366
1434
|
resolvedInputs[key] = key in userInputs ? userInputs[key] : def.default;
|
|
@@ -1706,113 +1774,131 @@ function registerJobsAPI(options) {
|
|
|
1706
1774
|
// src/api/cron-api.ts
|
|
1707
1775
|
function registerCronAPI(options) {
|
|
1708
1776
|
const { server, hostService, logger, observability } = options;
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1777
|
+
server.post(
|
|
1778
|
+
"/api/v1/crons",
|
|
1779
|
+
{ schema: { tags: ["Cron"], summary: "Register a cron job" } },
|
|
1780
|
+
async (request, reply) => {
|
|
1781
|
+
const tenantId = request.headers["x-tenant-id"] ?? "default";
|
|
1782
|
+
try {
|
|
1783
|
+
const data = await observability.observeOperation(
|
|
1784
|
+
"workflow.cron.register",
|
|
1785
|
+
() => Promise.resolve(hostService.registerCron(tenantId, request.body))
|
|
1786
|
+
);
|
|
1787
|
+
return ok(data);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
const message = error instanceof Error ? error.message : "Failed to register cron job";
|
|
1790
|
+
if (message === "Cron scheduler not available") {
|
|
1791
|
+
return fail(reply, 503, message);
|
|
1792
|
+
}
|
|
1793
|
+
if (message.startsWith("Missing required fields")) {
|
|
1794
|
+
return fail(reply, 400, message);
|
|
1795
|
+
}
|
|
1796
|
+
logger.error("Failed to register cron job", error instanceof Error ? error : void 0);
|
|
1797
|
+
return fail(reply, 500, message);
|
|
1724
1798
|
}
|
|
1725
|
-
logger.error("Failed to register cron job", error instanceof Error ? error : void 0);
|
|
1726
|
-
return fail(reply, 500, message);
|
|
1727
1799
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1800
|
+
);
|
|
1801
|
+
server.get(
|
|
1802
|
+
"/api/v1/crons",
|
|
1803
|
+
{ schema: { tags: ["Cron"], summary: "List cron jobs" } },
|
|
1804
|
+
async (_request, reply) => {
|
|
1805
|
+
try {
|
|
1806
|
+
return ok(await observability.observeOperation("workflow.cron.list", () => Promise.resolve(hostService.listCron())));
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
const message = error instanceof Error ? error.message : "Failed to list cron jobs";
|
|
1809
|
+
if (message === "Cron scheduler not available") {
|
|
1810
|
+
return fail(reply, 503, message);
|
|
1811
|
+
}
|
|
1812
|
+
logger.error("Failed to list cron jobs", error instanceof Error ? error : void 0);
|
|
1813
|
+
return fail(reply, 500, message);
|
|
1736
1814
|
}
|
|
1737
|
-
logger.error("Failed to list cron jobs", error instanceof Error ? error : void 0);
|
|
1738
|
-
return fail(reply, 500, message);
|
|
1739
1815
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1816
|
+
);
|
|
1817
|
+
server.delete(
|
|
1818
|
+
"/api/v1/crons/:id",
|
|
1819
|
+
{ schema: { tags: ["Cron"], summary: "Unregister a cron job" } },
|
|
1820
|
+
async (request, reply) => {
|
|
1821
|
+
const { id } = request.params;
|
|
1822
|
+
const tenantId = request.headers["x-tenant-id"] ?? "default";
|
|
1823
|
+
try {
|
|
1824
|
+
const data = await observability.observeOperation(
|
|
1825
|
+
"workflow.cron.unregister",
|
|
1826
|
+
() => Promise.resolve(hostService.unregisterCron(tenantId, id))
|
|
1827
|
+
);
|
|
1828
|
+
return ok(data);
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
const message = error instanceof Error ? error.message : "Failed to unregister cron job";
|
|
1831
|
+
if (message === "Cron scheduler not available") {
|
|
1832
|
+
return fail(reply, 503, message);
|
|
1833
|
+
}
|
|
1834
|
+
logger.error("Failed to unregister cron job", error instanceof Error ? error : void 0);
|
|
1835
|
+
return fail(reply, 500, message);
|
|
1754
1836
|
}
|
|
1755
|
-
logger.error("Failed to unregister cron job", error instanceof Error ? error : void 0);
|
|
1756
|
-
return fail(reply, 500, message);
|
|
1757
1837
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1838
|
+
);
|
|
1839
|
+
server.post(
|
|
1840
|
+
"/api/v1/crons/:id/trigger",
|
|
1841
|
+
{ schema: { tags: ["Cron"], summary: "Trigger a cron job immediately" } },
|
|
1842
|
+
async (request, reply) => {
|
|
1843
|
+
const { id } = request.params;
|
|
1844
|
+
const tenantId = request.headers["x-tenant-id"] ?? "default";
|
|
1845
|
+
try {
|
|
1846
|
+
const data = await observability.observeOperation("workflow.cron.trigger", () => hostService.triggerCron(tenantId, id));
|
|
1847
|
+
return ok(data);
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
const message = error instanceof Error ? error.message : "Failed to trigger cron job";
|
|
1850
|
+
if (message === "Cron scheduler not available") {
|
|
1851
|
+
return fail(reply, 503, message);
|
|
1852
|
+
}
|
|
1853
|
+
logger.error("Failed to trigger cron job", error instanceof Error ? error : void 0);
|
|
1854
|
+
return fail(reply, 500, message);
|
|
1769
1855
|
}
|
|
1770
|
-
logger.error("Failed to trigger cron job", error instanceof Error ? error : void 0);
|
|
1771
|
-
return fail(reply, 500, message);
|
|
1772
1856
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1857
|
+
);
|
|
1858
|
+
server.post(
|
|
1859
|
+
"/api/v1/crons/:id/pause",
|
|
1860
|
+
{ schema: { tags: ["Cron"], summary: "Pause a cron job" } },
|
|
1861
|
+
async (request, reply) => {
|
|
1862
|
+
const { id } = request.params;
|
|
1863
|
+
const tenantId = request.headers["x-tenant-id"] ?? "default";
|
|
1864
|
+
try {
|
|
1865
|
+
const data = await observability.observeOperation(
|
|
1866
|
+
"workflow.cron.pause",
|
|
1867
|
+
() => Promise.resolve(hostService.pauseCron(tenantId, id))
|
|
1868
|
+
);
|
|
1869
|
+
return ok(data);
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
const message = error instanceof Error ? error.message : "Failed to pause cron job";
|
|
1872
|
+
if (message === "Cron scheduler not available") {
|
|
1873
|
+
return fail(reply, 503, message);
|
|
1874
|
+
}
|
|
1875
|
+
logger.error("Failed to pause cron job", error instanceof Error ? error : void 0);
|
|
1876
|
+
return fail(reply, 500, message);
|
|
1787
1877
|
}
|
|
1788
|
-
logger.error("Failed to pause cron job", error instanceof Error ? error : void 0);
|
|
1789
|
-
return fail(reply, 500, message);
|
|
1790
1878
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1879
|
+
);
|
|
1880
|
+
server.post(
|
|
1881
|
+
"/api/v1/crons/:id/resume",
|
|
1882
|
+
{ schema: { tags: ["Cron"], summary: "Resume a cron job" } },
|
|
1883
|
+
async (request, reply) => {
|
|
1884
|
+
const { id } = request.params;
|
|
1885
|
+
const tenantId = request.headers["x-tenant-id"] ?? "default";
|
|
1886
|
+
try {
|
|
1887
|
+
const data = await observability.observeOperation(
|
|
1888
|
+
"workflow.cron.resume",
|
|
1889
|
+
() => Promise.resolve(hostService.resumeCron(tenantId, id))
|
|
1890
|
+
);
|
|
1891
|
+
return ok(data);
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
const message = error instanceof Error ? error.message : "Failed to resume cron job";
|
|
1894
|
+
if (message === "Cron scheduler not available") {
|
|
1895
|
+
return fail(reply, 503, message);
|
|
1896
|
+
}
|
|
1897
|
+
logger.error("Failed to resume cron job", error instanceof Error ? error : void 0);
|
|
1898
|
+
return fail(reply, 500, message);
|
|
1805
1899
|
}
|
|
1806
|
-
logger.error("Failed to resume cron job", error instanceof Error ? error : void 0);
|
|
1807
|
-
return fail(reply, 500, message);
|
|
1808
1900
|
}
|
|
1809
|
-
|
|
1810
|
-
server.post("/api/v1/crons", { schema: { tags: ["Cron"], summary: "Register a cron job" } }, registerCronHandler);
|
|
1811
|
-
server.get("/api/v1/crons", { schema: { tags: ["Cron"], summary: "List cron jobs" } }, listCronHandler);
|
|
1812
|
-
server.delete("/api/v1/crons/:id", { schema: { tags: ["Cron"], summary: "Unregister a cron job" } }, unregisterCronHandler);
|
|
1813
|
-
server.post("/api/v1/crons/:id/trigger", { schema: { tags: ["Cron"], summary: "Trigger a cron job immediately" } }, triggerCronHandler);
|
|
1814
|
-
server.post("/api/v1/crons/:id/pause", { schema: { tags: ["Cron"], summary: "Pause a cron job" } }, pauseCronHandler);
|
|
1815
|
-
server.post("/api/v1/crons/:id/resume", { schema: { tags: ["Cron"], summary: "Resume a cron job" } }, resumeCronHandler);
|
|
1901
|
+
);
|
|
1816
1902
|
}
|
|
1817
1903
|
|
|
1818
1904
|
// src/api/workflows-api.ts
|
|
@@ -1939,6 +2025,33 @@ function registerWorkflowsAPI(options) {
|
|
|
1939
2025
|
return fail(reply, 500, error instanceof Error ? error.message : "Failed to get run");
|
|
1940
2026
|
}
|
|
1941
2027
|
});
|
|
2028
|
+
server.get(
|
|
2029
|
+
"/api/v1/runs/:runId/logs",
|
|
2030
|
+
{ schema: { tags: ["Runs"], summary: "Get execution logs for a run" } },
|
|
2031
|
+
async (request, reply) => {
|
|
2032
|
+
const { runId } = request.params;
|
|
2033
|
+
const { stepId, level, limit, offset } = request.query;
|
|
2034
|
+
try {
|
|
2035
|
+
const logs = await observability.observeOperation(
|
|
2036
|
+
"workflow.run.logs",
|
|
2037
|
+
() => hostService.getRunLogs(runId, {
|
|
2038
|
+
stepId,
|
|
2039
|
+
level,
|
|
2040
|
+
limit: limit ? parseInt(limit, 10) : void 0,
|
|
2041
|
+
offset: offset ? parseInt(offset, 10) : void 0
|
|
2042
|
+
})
|
|
2043
|
+
);
|
|
2044
|
+
return ok({ logs, runId, stepId });
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
const message = error instanceof Error ? error.message : "Failed to get run logs";
|
|
2047
|
+
if (message === "Run not found") {
|
|
2048
|
+
return fail(reply, 404, message);
|
|
2049
|
+
}
|
|
2050
|
+
logger.error("[workflows-api] Error getting run logs", error instanceof Error ? error : void 0);
|
|
2051
|
+
return fail(reply, 500, message);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
);
|
|
1942
2055
|
server.get("/api/v1/runs/:runId/events", { schema: { hide: true } }, async (request, reply) => {
|
|
1943
2056
|
const { runId } = request.params;
|
|
1944
2057
|
const run = await observability.observeOperation("workflow.run.events", () => engine.getRun(runId));
|
|
@@ -2281,7 +2394,7 @@ async function createServer(options) {
|
|
|
2281
2394
|
});
|
|
2282
2395
|
server.get("/ready", async () => {
|
|
2283
2396
|
const metrics = await hostService.getMetrics();
|
|
2284
|
-
const checks =
|
|
2397
|
+
const checks = buildWorkflowReadinessChecks({ workflowService, cronScheduler, metrics });
|
|
2285
2398
|
const hasErrors = checks.some((entry) => entry.status === "error");
|
|
2286
2399
|
const hasWarnings = checks.some((entry) => entry.status === "warn");
|
|
2287
2400
|
return createServiceReadyResponse({
|
|
@@ -2333,6 +2446,16 @@ function resolveWorkflowHealthStatus(metrics) {
|
|
|
2333
2446
|
return "healthy";
|
|
2334
2447
|
}
|
|
2335
2448
|
function buildWorkflowChecks(input) {
|
|
2449
|
+
return [
|
|
2450
|
+
...buildWorkflowReadinessChecks(input),
|
|
2451
|
+
{
|
|
2452
|
+
id: "workflow-failures",
|
|
2453
|
+
status: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? "warn" : "ok",
|
|
2454
|
+
message: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? `${input.metrics.runs.failed} failed runs, ${input.metrics.jobs.failed} failed jobs retained in history` : "No failed workflow runs or jobs in retained history"
|
|
2455
|
+
}
|
|
2456
|
+
];
|
|
2457
|
+
}
|
|
2458
|
+
function buildWorkflowReadinessChecks(input) {
|
|
2336
2459
|
return [
|
|
2337
2460
|
{
|
|
2338
2461
|
id: "workflow-engine",
|
|
@@ -2348,11 +2471,6 @@ function buildWorkflowChecks(input) {
|
|
|
2348
2471
|
id: "cron-scheduler",
|
|
2349
2472
|
status: input.cronScheduler ? "ok" : "warn",
|
|
2350
2473
|
message: input.cronScheduler ? "Cron scheduler available" : "Cron scheduler not configured"
|
|
2351
|
-
},
|
|
2352
|
-
{
|
|
2353
|
-
id: "workflow-failures",
|
|
2354
|
-
status: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? "warn" : "ok",
|
|
2355
|
-
message: input.metrics.runs.failed > 0 || input.metrics.jobs.failed > 0 ? `${input.metrics.runs.failed} failed runs, ${input.metrics.jobs.failed} failed jobs retained in history` : "No failed workflow runs or jobs in retained history"
|
|
2356
2474
|
}
|
|
2357
2475
|
];
|
|
2358
2476
|
}
|
|
@@ -2429,7 +2547,13 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
2429
2547
|
executionId: startupSpanId
|
|
2430
2548
|
}
|
|
2431
2549
|
});
|
|
2432
|
-
|
|
2550
|
+
const debugMode = process.env["WORKFLOW_DEBUG"] === "true";
|
|
2551
|
+
bootstrapLogger.info("Workflow daemon starting", { repoRoot, projectRoot, debugMode });
|
|
2552
|
+
if (debugMode) {
|
|
2553
|
+
bootstrapLogger.warn(
|
|
2554
|
+
"[WORKFLOW_DEBUG=true] Verbose debug logging is ON \u2014 step inputs, outputs, and expr contexts will be logged. Disable in production (unset WORKFLOW_DEBUG or set to false)."
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2433
2557
|
const createWorkflowLogger = (service, operation, bindings) => createCorrelatedLogger(platform.logger, {
|
|
2434
2558
|
serviceId: "workflow",
|
|
2435
2559
|
logsSource: "workflow",
|
|
@@ -2499,7 +2623,7 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
2499
2623
|
logger: createWorkflowLogger("api", "workflow.api")
|
|
2500
2624
|
});
|
|
2501
2625
|
const port = parseInt(process.env.WORKFLOW_PORT || "7778", 10);
|
|
2502
|
-
await server.listen({ port, host: "0.0.0.0" });
|
|
2626
|
+
await server.listen({ port, host: process.env.WORKFLOW_HOST ?? "0.0.0.0" });
|
|
2503
2627
|
bootstrapLogger.info("HTTP API listening", { port });
|
|
2504
2628
|
serverInstance = server;
|
|
2505
2629
|
bootstrapLogger.info("Creating WorkflowWorker");
|
|
@@ -2510,7 +2634,8 @@ async function bootstrap(cwd = process.cwd()) {
|
|
|
2510
2634
|
analytics: platform.analytics,
|
|
2511
2635
|
platform,
|
|
2512
2636
|
workspaceRoot: projectRoot,
|
|
2513
|
-
concurrency: parseInt(process.env.WORKFLOW_CONCURRENCY || "5", 10)
|
|
2637
|
+
concurrency: parseInt(process.env.WORKFLOW_CONCURRENCY || "5", 10),
|
|
2638
|
+
debugMode
|
|
2514
2639
|
});
|
|
2515
2640
|
workerInstance = worker;
|
|
2516
2641
|
bootstrapLogger.info("Starting WorkflowWorker");
|