@openfn/ws-worker 1.19.7 → 1.20.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # ws-worker
2
2
 
3
+ ## 1.20.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [a1bfdc1]
8
+ - Updated dependencies [c70369b]
9
+ - @openfn/runtime@1.7.7
10
+ - @openfn/logger@1.1.1
11
+ - @openfn/engine-multi@1.9.1
12
+
13
+ ## 1.20.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 3a7038b: Allow logs to be sent to Lightning in batches. This behavior is disabled by default for back-compatibility: set `WORKER_BATCH_LOGS=true` to enable.
18
+ - 5417e97: Reduce the payload size of log events sent out to Lightning. This defaults to 1mb but can be configured with `WORKER_MAX_LOG_PAYLOAD_MB`
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [5417e97]
23
+ - @openfn/lexicon@1.3.0
24
+ - @openfn/engine-multi@1.9.0
25
+
3
26
  ## 1.19.7
4
27
 
5
28
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -49,9 +49,13 @@ interface Channel extends Channel$1 {
49
49
  type WorkerRunOptions = ExecuteOptions & {
50
50
  outputDataclips?: boolean;
51
51
  payloadLimitMb?: number;
52
+ logPayloadLimitMb?: number;
52
53
  jobLogLevel?: LogLevel;
53
54
  timeoutRetryCount?: number;
54
55
  timeoutRetryDelay?: number;
56
+ batchLogs?: boolean;
57
+ batchInterval?: number;
58
+ batchLimit?: number;
55
59
  };
56
60
 
57
61
  type Context = {
@@ -65,6 +69,9 @@ type Context = {
65
69
  };
66
70
 
67
71
  type ServerOptions = {
72
+ batchLogs?: boolean;
73
+ batchInterval?: number;
74
+ batchLimit?: number;
68
75
  maxWorkflows?: number;
69
76
  port?: number;
70
77
  lightning?: string;
@@ -82,6 +89,7 @@ type ServerOptions = {
82
89
  messageTimeoutSeconds?: number;
83
90
  claimTimeoutSeconds?: number;
84
91
  payloadLimitMb?: number;
92
+ logPayloadLimitMb?: number;
85
93
  collectionsVersion?: string;
86
94
  collectionsUrl?: string;
87
95
  monorepoDir?: string;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { EventEmitter as EventEmitter2 } from "node:events";
3
3
  import { promisify } from "node:util";
4
4
  import { exec as _exec } from "node:child_process";
5
- import * as Sentry6 from "@sentry/node";
5
+ import * as Sentry7 from "@sentry/node";
6
6
  import Koa from "koa";
7
7
  import bodyParser from "koa-bodyparser";
8
8
  import koaLogger from "koa-logger";
@@ -271,7 +271,16 @@ var startWorkloop = (app, logger, minBackoff, maxBackoff, maxWorkers) => {
271
271
  var workloop_default = startWorkloop;
272
272
 
273
273
  // src/api/execute.ts
274
- import * as Sentry3 from "@sentry/node";
274
+ import * as Sentry4 from "@sentry/node";
275
+ import {
276
+ JOB_COMPLETE as JOB_COMPLETE2,
277
+ JOB_START as JOB_START2,
278
+ WORKFLOW_COMPLETE as WORKFLOW_COMPLETE2,
279
+ WORKFLOW_LOG as WORKFLOW_LOG2,
280
+ WORKFLOW_START as WORKFLOW_START2,
281
+ JOB_ERROR as JOB_ERROR2,
282
+ WORKFLOW_ERROR as WORKFLOW_ERROR2
283
+ } from "@openfn/engine-multi";
275
284
 
276
285
  // src/util/convert-lightning-plan.ts
277
286
  import crypto2 from "node:crypto";
@@ -342,6 +351,9 @@ var convert_lightning_plan_default = (run, options = {}) => {
342
351
  if ("payload_limit_mb" in run.options) {
343
352
  engineOpts.payloadLimitMb = run.options.payload_limit_mb;
344
353
  }
354
+ if ("log_payload_limit_mb" in run.options) {
355
+ engineOpts.logPayloadLimitMb = run.options.log_payload_limit_mb;
356
+ }
345
357
  if ("run_memory_limit_mb" in run.options) {
346
358
  engineOpts.memoryLimitMb = run.options.run_memory_limit_mb;
347
359
  }
@@ -576,43 +588,59 @@ var stringify_default = (obj) => stringify(obj, (_key, value) => {
576
588
  return value;
577
589
  });
578
590
 
579
- // src/util/throttle.ts
580
- var createThrottler = () => {
581
- const q = [];
582
- let activePromise;
583
- const add = (fn) => {
584
- return (...args) => (
585
- // the wrapped function is a deferred promise
586
- // that will reject or resolve as usual
587
- new Promise((resolve, reject) => {
588
- q.push({ fn, args, resolve, reject });
589
- shift();
590
- })
591
- );
592
- };
593
- const shift = () => {
594
- if (activePromise) {
595
- return;
596
- }
597
- const next = q.shift();
598
- if (next) {
599
- const { fn, args, resolve, reject } = next;
600
- activePromise = fn(...args).then(resolve).catch(reject).finally(() => {
601
- activePromise = void 0;
602
- shift();
603
- });
604
- }
605
- };
606
- return add;
607
- };
608
- var throttle_default = createThrottler;
609
-
610
591
  // src/util/timestamp.ts
611
592
  var timeInMicroseconds = (time) => time && (BigInt(time) / BigInt(1e3)).toString();
612
593
 
613
594
  // src/events/run-start.ts
614
595
  import { timestamp } from "@openfn/logger";
615
596
 
597
+ // src/events/run-log.ts
598
+ async function onRunLog(context, events) {
599
+ if (!Array.isArray(events)) {
600
+ events = [events];
601
+ }
602
+ const { state, options } = context;
603
+ const { batchLogs } = options ?? {};
604
+ const logs = events.map((evt) => {
605
+ let message = evt.message;
606
+ if (evt.redacted) {
607
+ message = [
608
+ `(Log message redacted: exceeds ${options.logPayloadLimitMb ?? options.payloadLimitMb}mb memory limit)`
609
+ ];
610
+ } else if (typeof evt.message === "string") {
611
+ message = JSON.parse(evt.message);
612
+ }
613
+ const logLine = {
614
+ message,
615
+ source: evt.name,
616
+ level: evt.level,
617
+ timestamp: timeInMicroseconds(evt.time)
618
+ };
619
+ if (state.activeStep) {
620
+ logLine.step_id = state.activeStep;
621
+ }
622
+ return logLine;
623
+ });
624
+ if (batchLogs) {
625
+ const payload = {
626
+ run_id: `${state.plan.id}`,
627
+ logs
628
+ };
629
+ return sendEvent(context, RUN_LOG, payload);
630
+ } else {
631
+ return new Promise(async (resolve) => {
632
+ for (const log of logs) {
633
+ const payload = {
634
+ run_id: `${state.plan.id}`,
635
+ ...log
636
+ };
637
+ await sendEvent(context, RUN_LOG, payload);
638
+ }
639
+ resolve();
640
+ });
641
+ }
642
+ }
643
+
616
644
  // src/util/versions.ts
617
645
  import { mainSymbols } from "figures";
618
646
  var { triangleRightSmall: t } = mainSymbols;
@@ -680,21 +708,25 @@ async function onRunStart(context, event) {
680
708
  timestamp: timeInMicroseconds(event.time)
681
709
  });
682
710
  if ("payloadLimitMb" in options) {
683
- await onJobLog(versionLogContext, {
684
- // use the fake time in the log
685
- time,
686
- message: [`Payload limit: ${options.payloadLimitMb}mb`],
687
- level: "info",
688
- name: "RTE"
689
- });
711
+ await onRunLog(versionLogContext, [
712
+ {
713
+ // use the fake time in the log
714
+ time,
715
+ message: [`Payload limit: ${options.payloadLimitMb}mb`],
716
+ level: "info",
717
+ name: "RTE"
718
+ }
719
+ ]);
690
720
  }
691
721
  const versionMessage = versions_default(versions);
692
- await onJobLog(versionLogContext, {
693
- time,
694
- message: [versionMessage],
695
- level: "info",
696
- name: "VER"
697
- });
722
+ await onRunLog(versionLogContext, [
723
+ {
724
+ time,
725
+ message: [versionMessage],
726
+ level: "info",
727
+ name: "VER"
728
+ }
729
+ ]);
698
730
  }
699
731
 
700
732
  // src/events/step-complete.ts
@@ -763,14 +795,16 @@ async function onStepComplete(context, event, error) {
763
795
  state.withheldDataclips[dataclipId] = true;
764
796
  evt.output_dataclip_error = "DATACLIP_TOO_LARGE";
765
797
  const time = (timestamp2() - BigInt(1e7)).toString();
766
- await onJobLog(context, {
767
- time,
768
- message: [
769
- "Dataclip exceeds payload limit: output will not be sent back to the app."
770
- ],
771
- level: "info",
772
- name: "R/T"
773
- });
798
+ await onRunLog(context, [
799
+ {
800
+ time,
801
+ message: [
802
+ "Dataclip exceeds payload limit: output will not be sent back to the app."
803
+ ],
804
+ level: "info",
805
+ name: "R/T"
806
+ }
807
+ ]);
774
808
  } else {
775
809
  evt.output_dataclip_id = dataclipId;
776
810
  if (!options || options.outputDataclips !== false) {
@@ -807,20 +841,24 @@ import { timestamp as timestamp3 } from "@openfn/logger";
807
841
  var log_final_reason_default = async (context, reason) => {
808
842
  const time = (timestamp3() - BigInt(1e7)).toString();
809
843
  const statusMessage = `Run complete with status: ${reason.reason}`;
810
- await onJobLog(context, {
811
- time,
812
- message: [statusMessage],
813
- level: "info",
814
- name: "R/T"
815
- });
816
- if (reason.reason !== "success") {
817
- const errorMessage = `${reason.error_type}: ${reason.error_message || "unknown"}`;
818
- await onJobLog(context, {
844
+ await onRunLog(context, [
845
+ {
819
846
  time,
820
- message: [errorMessage],
847
+ message: [statusMessage],
821
848
  level: "info",
822
849
  name: "R/T"
823
- });
850
+ }
851
+ ]);
852
+ if (reason.reason !== "success") {
853
+ const errorMessage = `${reason.error_type}: ${reason.error_message || "unknown"}`;
854
+ await onRunLog(context, [
855
+ {
856
+ time,
857
+ message: [errorMessage],
858
+ level: "info",
859
+ name: "R/T"
860
+ }
861
+ ]);
824
862
  }
825
863
  };
826
864
 
@@ -865,15 +903,137 @@ async function onRunError(context, event) {
865
903
  }
866
904
  }
867
905
 
868
- // src/api/execute.ts
869
- var enc = new TextDecoder("utf-8");
906
+ // src/api/process-events.ts
907
+ import * as Sentry3 from "@sentry/node";
908
+ import {
909
+ JOB_COMPLETE,
910
+ JOB_ERROR,
911
+ JOB_START,
912
+ WORKFLOW_COMPLETE,
913
+ WORKFLOW_ERROR,
914
+ WORKFLOW_LOG,
915
+ WORKFLOW_START
916
+ } from "@openfn/engine-multi";
917
+ var DEFAULT_BATCH_LIMIT = 10;
918
+ var DEFAULT_BATCH_INTERVAL = 10;
870
919
  var eventMap = {
871
- "workflow-start": RUN_START,
872
- "job-start": STEP_START,
873
- "job-complete": STEP_COMPLETE,
874
- "workflow-log": RUN_LOG,
875
- "workflow-complete": RUN_COMPLETE
920
+ [WORKFLOW_START]: RUN_START,
921
+ [JOB_START]: STEP_START,
922
+ [JOB_COMPLETE]: STEP_COMPLETE,
923
+ [WORKFLOW_LOG]: RUN_LOG,
924
+ [WORKFLOW_COMPLETE]: RUN_COMPLETE
876
925
  };
926
+ var allEngineEvents = [
927
+ WORKFLOW_START,
928
+ WORKFLOW_COMPLETE,
929
+ JOB_START,
930
+ JOB_COMPLETE,
931
+ WORKFLOW_LOG,
932
+ JOB_ERROR,
933
+ WORKFLOW_ERROR
934
+ ];
935
+ function eventProcessor(engine, context, callbacks, options = {}) {
936
+ const { id: planId, logger } = context;
937
+ const {
938
+ batchLimit: limit = DEFAULT_BATCH_LIMIT,
939
+ batchInterval: interval = DEFAULT_BATCH_INTERVAL
940
+ } = options;
941
+ const queue = [];
942
+ const next = async () => {
943
+ const evt = queue[0];
944
+ if (evt) {
945
+ await process2(evt.name, evt.event);
946
+ queue.shift();
947
+ next();
948
+ }
949
+ };
950
+ let activeBatch = null;
951
+ let batch = [];
952
+ let start = -1;
953
+ let batchTimeout;
954
+ const sendBatch = async (name) => {
955
+ clearTimeout(batchTimeout);
956
+ activeBatch = null;
957
+ await send(name, batch, batch.length);
958
+ batch = [];
959
+ };
960
+ const send = async (name, payload, batchSize) => {
961
+ const lightningEvent = eventMap[name] ?? name;
962
+ await callbacks[name](context, payload);
963
+ if (batchSize) {
964
+ logger.info(
965
+ `${planId} :: sent ${lightningEvent} (${batchSize}):: OK :: ${Date.now() - start}ms`
966
+ );
967
+ } else {
968
+ logger.info(
969
+ `${planId} :: sent ${lightningEvent} :: OK :: ${Date.now() - start}ms`
970
+ );
971
+ }
972
+ };
973
+ const process2 = async (name, event) => {
974
+ if (name !== "workflow-log") {
975
+ Sentry3.addBreadcrumb({
976
+ category: "event",
977
+ message: name,
978
+ level: "info"
979
+ });
980
+ }
981
+ if (name === activeBatch) {
982
+ batch.push(event);
983
+ if (batch.length >= limit) {
984
+ await sendBatch(name);
985
+ }
986
+ return;
987
+ } else if (activeBatch) {
988
+ await sendBatch(activeBatch);
989
+ }
990
+ if (name in callbacks) {
991
+ try {
992
+ start = Date.now();
993
+ if (options?.batch?.[name]) {
994
+ activeBatch = name;
995
+ batch.push(event);
996
+ while (queue.length > 1 && queue[1].name === name) {
997
+ const [nextBatchItem] = queue.splice(1, 1);
998
+ batch.push(nextBatchItem.event);
999
+ if (batch.length >= limit) {
1000
+ return sendBatch(name);
1001
+ }
1002
+ }
1003
+ if (!batchTimeout) {
1004
+ const batchName = activeBatch;
1005
+ batchTimeout = setTimeout(async () => {
1006
+ sendBatch(batchName);
1007
+ }, interval);
1008
+ }
1009
+ return;
1010
+ }
1011
+ await send(name, event);
1012
+ } catch (e2) {
1013
+ if (!e2.reportedToSentry) {
1014
+ Sentry3.captureException(e2);
1015
+ logger.error(e2);
1016
+ }
1017
+ }
1018
+ } else {
1019
+ logger.warn("no event bound for", name);
1020
+ }
1021
+ };
1022
+ const enqueue = (name, event) => {
1023
+ queue.push({ name, event });
1024
+ if (queue.length == 1) {
1025
+ next();
1026
+ }
1027
+ };
1028
+ const e = allEngineEvents.reduce(
1029
+ (obj, e2) => Object.assign(obj, { [e2]: (p) => enqueue(e2, p) }),
1030
+ {}
1031
+ );
1032
+ engine.listen(planId, e);
1033
+ }
1034
+
1035
+ // src/api/execute.ts
1036
+ var enc = new TextDecoder("utf-8");
877
1037
  function execute(channel, engine, logger, plan, input, options = {}, onFinish = (_result) => {
878
1038
  }) {
879
1039
  logger.info("executing ", plan.id);
@@ -887,8 +1047,8 @@ function execute(channel, engine, logger, plan, input, options = {}, onFinish =
887
1047
  options,
888
1048
  onFinish
889
1049
  };
890
- Sentry3.withIsolationScope(async () => {
891
- Sentry3.addBreadcrumb({
1050
+ Sentry4.withIsolationScope(async () => {
1051
+ Sentry4.addBreadcrumb({
892
1052
  category: "run",
893
1053
  message: "Executing run: loading metadata",
894
1054
  level: "info",
@@ -896,47 +1056,26 @@ function execute(channel, engine, logger, plan, input, options = {}, onFinish =
896
1056
  runId: plan.id
897
1057
  }
898
1058
  });
899
- const throttle = throttle_default();
900
- const addEvent = (eventName, handler) => {
901
- const wrappedFn = async (event) => {
902
- if (eventName !== "workflow-log") {
903
- Sentry3.addBreadcrumb({
904
- category: "event",
905
- message: eventName,
906
- level: "info"
907
- });
908
- }
909
- const lightningEvent = eventMap[eventName] ?? eventName;
910
- try {
911
- let start = Date.now();
912
- await handler(context, event);
913
- logger.info(
914
- `${plan.id} :: sent ${lightningEvent} :: OK :: ${Date.now() - start}ms`
915
- );
916
- } catch (e) {
917
- if (!e.reportedToSentry) {
918
- Sentry3.captureException(e);
919
- logger.error(e);
920
- }
921
- }
922
- };
923
- return {
924
- [eventName]: wrappedFn
925
- };
926
- };
927
- const listeners = Object.assign(
928
- {},
929
- addEvent("workflow-start", throttle(onRunStart)),
930
- addEvent("job-start", throttle(onStepStart)),
931
- addEvent("job-complete", throttle(onStepComplete)),
932
- addEvent("job-error", throttle(onJobError)),
933
- addEvent("workflow-log", throttle(onJobLog)),
934
- // This will also resolve the promise
935
- addEvent("workflow-complete", throttle(onWorkflowComplete)),
936
- addEvent("workflow-error", throttle(onRunError))
937
- // TODO send autoinstall logs
1059
+ eventProcessor(
1060
+ engine,
1061
+ context,
1062
+ {
1063
+ [WORKFLOW_START2]: onRunStart,
1064
+ [JOB_START2]: onStepStart,
1065
+ [JOB_COMPLETE2]: onStepComplete,
1066
+ [JOB_ERROR2]: onJobError,
1067
+ [WORKFLOW_LOG2]: onRunLog,
1068
+ [WORKFLOW_COMPLETE2]: onWorkflowComplete,
1069
+ [WORKFLOW_ERROR2]: onRunError
1070
+ },
1071
+ {
1072
+ batch: options.batchLogs ? {
1073
+ [WORKFLOW_LOG2]: true
1074
+ } : {},
1075
+ batchInterval: options.batchInterval,
1076
+ batchLimit: options.batchLimit
1077
+ }
938
1078
  );
939
- engine.listen(plan.id, listeners);
940
1079
  const resolvers = {
941
1080
  credential: (id) => loadCredential(context, id)
942
1081
  // TODO not supported right now
@@ -959,7 +1098,7 @@ function execute(channel, engine, logger, plan, input, options = {}, onFinish =
959
1098
  }
960
1099
  }
961
1100
  try {
962
- Sentry3.addBreadcrumb({
1101
+ Sentry4.addBreadcrumb({
963
1102
  category: "run",
964
1103
  message: "run metadata loaded: starting run",
965
1104
  level: "info",
@@ -969,7 +1108,7 @@ function execute(channel, engine, logger, plan, input, options = {}, onFinish =
969
1108
  });
970
1109
  engine.execute(plan, loadedInput, { resolvers, ...options });
971
1110
  } catch (e) {
972
- Sentry3.addBreadcrumb({
1111
+ Sentry4.addBreadcrumb({
973
1112
  category: "run",
974
1113
  message: "exception in run",
975
1114
  level: "info",
@@ -996,29 +1135,6 @@ function onJobError(context, event) {
996
1135
  return onStepComplete(context, event, event.error);
997
1136
  }
998
1137
  }
999
- function onJobLog(context, event) {
1000
- const { state, options } = context;
1001
- let message = event.message;
1002
- if (event.redacted) {
1003
- message = [
1004
- `(Log message redacted: exceeds ${options.payloadLimitMb}mb memory limit)`
1005
- ];
1006
- } else if (typeof event.message === "string") {
1007
- message = JSON.parse(event.message);
1008
- }
1009
- const log = {
1010
- run_id: `${state.plan.id}`,
1011
- message,
1012
- source: event.name,
1013
- level: event.level,
1014
- // @ts-ignore
1015
- timestamp: timeInMicroseconds(event.time)
1016
- };
1017
- if (state.activeStep) {
1018
- log.step_id = state.activeStep;
1019
- }
1020
- return sendEvent(context, RUN_LOG, log);
1021
- }
1022
1138
  async function loadDataclip(context, stateId) {
1023
1139
  const result = await sendEvent(context, GET_DATACLIP, {
1024
1140
  id: stateId
@@ -1036,7 +1152,7 @@ var healthcheck_default = (ctx) => {
1036
1152
  };
1037
1153
 
1038
1154
  // src/channels/run.ts
1039
- import * as Sentry4 from "@sentry/node";
1155
+ import * as Sentry5 from "@sentry/node";
1040
1156
  var joinRunChannel = (socket, token, runId, logger, timeout = 30) => {
1041
1157
  return new Promise((resolve, reject) => {
1042
1158
  let didReceiveOk = false;
@@ -1055,12 +1171,12 @@ var joinRunChannel = (socket, token, runId, logger, timeout = 30) => {
1055
1171
  resolve({ channel, run });
1056
1172
  }
1057
1173
  }).receive("error", (err) => {
1058
- Sentry4.captureException(err);
1174
+ Sentry5.captureException(err);
1059
1175
  logger.error(`error connecting to ${channelName}`, err);
1060
1176
  channel?.leave();
1061
1177
  reject(err);
1062
1178
  }).receive("timeout", (err) => {
1063
- Sentry4.captureException(err);
1179
+ Sentry5.captureException(err);
1064
1180
  logger.error(`Timeout for ${channelName}`, err);
1065
1181
  channel?.leave();
1066
1182
  reject(err);
@@ -1077,7 +1193,7 @@ var run_default = joinRunChannel;
1077
1193
 
1078
1194
  // src/channels/worker-queue.ts
1079
1195
  import EventEmitter from "node:events";
1080
- import * as Sentry5 from "@sentry/node";
1196
+ import * as Sentry6 from "@sentry/node";
1081
1197
  import { Socket as PhxSocket } from "phoenix";
1082
1198
  import { WebSocket } from "ws";
1083
1199
  import { API_VERSION } from "@openfn/lexicon/lightning";
@@ -1116,13 +1232,13 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
1116
1232
  SocketConstructor = PhxSocket
1117
1233
  } = options;
1118
1234
  const events = new EventEmitter();
1119
- Sentry5.addBreadcrumb({
1235
+ Sentry6.addBreadcrumb({
1120
1236
  category: "lifecycle",
1121
1237
  message: "Connecting to worker queue",
1122
1238
  level: "info"
1123
1239
  });
1124
1240
  worker_token_default(secret, serverId, logger).then(async (token) => {
1125
- Sentry5.addBreadcrumb({
1241
+ Sentry6.addBreadcrumb({
1126
1242
  category: "lifecycle",
1127
1243
  message: "Worker token generated",
1128
1244
  level: "info"
@@ -1141,7 +1257,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
1141
1257
  let didOpen = false;
1142
1258
  let shouldReportConnectionError = true;
1143
1259
  socket.onOpen(() => {
1144
- Sentry5.addBreadcrumb({
1260
+ Sentry6.addBreadcrumb({
1145
1261
  category: "lifecycle",
1146
1262
  message: "Web socket connected",
1147
1263
  level: "info"
@@ -1168,7 +1284,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
1168
1284
  events.emit("disconnect");
1169
1285
  });
1170
1286
  socket.onError((e) => {
1171
- Sentry5.addBreadcrumb({
1287
+ Sentry6.addBreadcrumb({
1172
1288
  category: "lifecycle",
1173
1289
  message: "Error in web socket connection",
1174
1290
  level: "info"
@@ -1176,7 +1292,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
1176
1292
  if (shouldReportConnectionError) {
1177
1293
  logger.debug("Reporting connection error to sentry");
1178
1294
  shouldReportConnectionError = false;
1179
- Sentry5.captureException(e);
1295
+ Sentry6.captureException(e);
1180
1296
  }
1181
1297
  if (!didOpen) {
1182
1298
  events.emit("error", e.message);
@@ -1286,11 +1402,11 @@ function createServer(engine, options = {}) {
1286
1402
  app.events = new EventEmitter2();
1287
1403
  app.engine = engine;
1288
1404
  if (options.sentryDsn) {
1289
- Sentry6.init({
1405
+ Sentry7.init({
1290
1406
  environment: options.sentryEnv,
1291
1407
  dsn: options.sentryDsn
1292
1408
  });
1293
- Sentry6.setupKoaErrorHandler(app);
1409
+ Sentry7.setupKoaErrorHandler(app);
1294
1410
  }
1295
1411
  app.use(bodyParser());
1296
1412
  app.use(
@@ -1347,8 +1463,14 @@ function createServer(engine, options = {}) {
1347
1463
  if (!("payloadLimitMb" in options2)) {
1348
1464
  options2.payloadLimitMb = app.options.payloadLimitMb;
1349
1465
  }
1466
+ if (!("logPayloadLimitMb" in options2)) {
1467
+ options2.logPayloadLimitMb = app.options.logPayloadLimitMb;
1468
+ }
1350
1469
  options2.timeoutRetryCount = app.options.timeoutRetryCount;
1351
1470
  options2.timeoutRetryDelay = app.options.timeoutRetryDelayMs ?? app.options.socketTimeoutSeconds;
1471
+ options2.batchLogs = app.options.batchLogs;
1472
+ options2.batchInterval = app.options.batchInterval;
1473
+ options2.batchLimit = app.options.batchLimit;
1352
1474
  const onFinish = () => {
1353
1475
  const duration = (Date.now() - start) / 1e3;
1354
1476
  logger.debug(
package/dist/start.js CHANGED
@@ -151,7 +151,7 @@ var runtime_engine_default = createMock;
151
151
  import { EventEmitter as EventEmitter3 } from "node:events";
152
152
  import { promisify } from "node:util";
153
153
  import { exec as _exec } from "node:child_process";
154
- import * as Sentry6 from "@sentry/node";
154
+ import * as Sentry7 from "@sentry/node";
155
155
  import Koa from "koa";
156
156
  import bodyParser from "koa-bodyparser";
157
157
  import koaLogger from "koa-logger";
@@ -420,7 +420,16 @@ var startWorkloop = (app, logger2, minBackoff2, maxBackoff2, maxWorkers) => {
420
420
  var workloop_default = startWorkloop;
421
421
 
422
422
  // src/api/execute.ts
423
- import * as Sentry3 from "@sentry/node";
423
+ import * as Sentry4 from "@sentry/node";
424
+ import {
425
+ JOB_COMPLETE as JOB_COMPLETE2,
426
+ JOB_START as JOB_START2,
427
+ WORKFLOW_COMPLETE as WORKFLOW_COMPLETE2,
428
+ WORKFLOW_LOG as WORKFLOW_LOG2,
429
+ WORKFLOW_START as WORKFLOW_START2,
430
+ JOB_ERROR as JOB_ERROR2,
431
+ WORKFLOW_ERROR as WORKFLOW_ERROR2
432
+ } from "@openfn/engine-multi";
424
433
 
425
434
  // src/util/convert-lightning-plan.ts
426
435
  import crypto3 from "node:crypto";
@@ -491,6 +500,9 @@ var convert_lightning_plan_default = (run2, options = {}) => {
491
500
  if ("payload_limit_mb" in run2.options) {
492
501
  engineOpts.payloadLimitMb = run2.options.payload_limit_mb;
493
502
  }
503
+ if ("log_payload_limit_mb" in run2.options) {
504
+ engineOpts.logPayloadLimitMb = run2.options.log_payload_limit_mb;
505
+ }
494
506
  if ("run_memory_limit_mb" in run2.options) {
495
507
  engineOpts.memoryLimitMb = run2.options.run_memory_limit_mb;
496
508
  }
@@ -725,43 +737,59 @@ var stringify_default = (obj) => stringify(obj, (_key, value) => {
725
737
  return value;
726
738
  });
727
739
 
728
- // src/util/throttle.ts
729
- var createThrottler = () => {
730
- const q = [];
731
- let activePromise;
732
- const add = (fn) => {
733
- return (...args2) => (
734
- // the wrapped function is a deferred promise
735
- // that will reject or resolve as usual
736
- new Promise((resolve5, reject) => {
737
- q.push({ fn, args: args2, resolve: resolve5, reject });
738
- shift();
739
- })
740
- );
741
- };
742
- const shift = () => {
743
- if (activePromise) {
744
- return;
745
- }
746
- const next = q.shift();
747
- if (next) {
748
- const { fn, args: args2, resolve: resolve5, reject } = next;
749
- activePromise = fn(...args2).then(resolve5).catch(reject).finally(() => {
750
- activePromise = void 0;
751
- shift();
752
- });
753
- }
754
- };
755
- return add;
756
- };
757
- var throttle_default = createThrottler;
758
-
759
740
  // src/util/timestamp.ts
760
741
  var timeInMicroseconds = (time) => time && (BigInt(time) / BigInt(1e3)).toString();
761
742
 
762
743
  // src/events/run-start.ts
763
744
  import { timestamp } from "@openfn/logger";
764
745
 
746
+ // src/events/run-log.ts
747
+ async function onRunLog(context, events) {
748
+ if (!Array.isArray(events)) {
749
+ events = [events];
750
+ }
751
+ const { state, options } = context;
752
+ const { batchLogs } = options ?? {};
753
+ const logs = events.map((evt) => {
754
+ let message = evt.message;
755
+ if (evt.redacted) {
756
+ message = [
757
+ `(Log message redacted: exceeds ${options.logPayloadLimitMb ?? options.payloadLimitMb}mb memory limit)`
758
+ ];
759
+ } else if (typeof evt.message === "string") {
760
+ message = JSON.parse(evt.message);
761
+ }
762
+ const logLine = {
763
+ message,
764
+ source: evt.name,
765
+ level: evt.level,
766
+ timestamp: timeInMicroseconds(evt.time)
767
+ };
768
+ if (state.activeStep) {
769
+ logLine.step_id = state.activeStep;
770
+ }
771
+ return logLine;
772
+ });
773
+ if (batchLogs) {
774
+ const payload = {
775
+ run_id: `${state.plan.id}`,
776
+ logs
777
+ };
778
+ return sendEvent(context, RUN_LOG, payload);
779
+ } else {
780
+ return new Promise(async (resolve5) => {
781
+ for (const log of logs) {
782
+ const payload = {
783
+ run_id: `${state.plan.id}`,
784
+ ...log
785
+ };
786
+ await sendEvent(context, RUN_LOG, payload);
787
+ }
788
+ resolve5();
789
+ });
790
+ }
791
+ }
792
+
765
793
  // src/util/versions.ts
766
794
  import { mainSymbols } from "figures";
767
795
  var { triangleRightSmall: t } = mainSymbols;
@@ -829,21 +857,25 @@ async function onRunStart(context, event) {
829
857
  timestamp: timeInMicroseconds(event.time)
830
858
  });
831
859
  if ("payloadLimitMb" in options) {
832
- await onJobLog(versionLogContext, {
833
- // use the fake time in the log
834
- time,
835
- message: [`Payload limit: ${options.payloadLimitMb}mb`],
836
- level: "info",
837
- name: "RTE"
838
- });
860
+ await onRunLog(versionLogContext, [
861
+ {
862
+ // use the fake time in the log
863
+ time,
864
+ message: [`Payload limit: ${options.payloadLimitMb}mb`],
865
+ level: "info",
866
+ name: "RTE"
867
+ }
868
+ ]);
839
869
  }
840
870
  const versionMessage = versions_default(versions);
841
- await onJobLog(versionLogContext, {
842
- time,
843
- message: [versionMessage],
844
- level: "info",
845
- name: "VER"
846
- });
871
+ await onRunLog(versionLogContext, [
872
+ {
873
+ time,
874
+ message: [versionMessage],
875
+ level: "info",
876
+ name: "VER"
877
+ }
878
+ ]);
847
879
  }
848
880
 
849
881
  // src/events/step-complete.ts
@@ -912,14 +944,16 @@ async function onStepComplete(context, event, error) {
912
944
  state.withheldDataclips[dataclipId] = true;
913
945
  evt.output_dataclip_error = "DATACLIP_TOO_LARGE";
914
946
  const time = (timestamp2() - BigInt(1e7)).toString();
915
- await onJobLog(context, {
916
- time,
917
- message: [
918
- "Dataclip exceeds payload limit: output will not be sent back to the app."
919
- ],
920
- level: "info",
921
- name: "R/T"
922
- });
947
+ await onRunLog(context, [
948
+ {
949
+ time,
950
+ message: [
951
+ "Dataclip exceeds payload limit: output will not be sent back to the app."
952
+ ],
953
+ level: "info",
954
+ name: "R/T"
955
+ }
956
+ ]);
923
957
  } else {
924
958
  evt.output_dataclip_id = dataclipId;
925
959
  if (!options || options.outputDataclips !== false) {
@@ -956,20 +990,24 @@ import { timestamp as timestamp3 } from "@openfn/logger";
956
990
  var log_final_reason_default = async (context, reason) => {
957
991
  const time = (timestamp3() - BigInt(1e7)).toString();
958
992
  const statusMessage = `Run complete with status: ${reason.reason}`;
959
- await onJobLog(context, {
960
- time,
961
- message: [statusMessage],
962
- level: "info",
963
- name: "R/T"
964
- });
965
- if (reason.reason !== "success") {
966
- const errorMessage = `${reason.error_type}: ${reason.error_message || "unknown"}`;
967
- await onJobLog(context, {
993
+ await onRunLog(context, [
994
+ {
968
995
  time,
969
- message: [errorMessage],
996
+ message: [statusMessage],
970
997
  level: "info",
971
998
  name: "R/T"
972
- });
999
+ }
1000
+ ]);
1001
+ if (reason.reason !== "success") {
1002
+ const errorMessage = `${reason.error_type}: ${reason.error_message || "unknown"}`;
1003
+ await onRunLog(context, [
1004
+ {
1005
+ time,
1006
+ message: [errorMessage],
1007
+ level: "info",
1008
+ name: "R/T"
1009
+ }
1010
+ ]);
973
1011
  }
974
1012
  };
975
1013
 
@@ -1014,15 +1052,137 @@ async function onRunError(context, event) {
1014
1052
  }
1015
1053
  }
1016
1054
 
1017
- // src/api/execute.ts
1018
- var enc = new TextDecoder("utf-8");
1055
+ // src/api/process-events.ts
1056
+ import * as Sentry3 from "@sentry/node";
1057
+ import {
1058
+ JOB_COMPLETE,
1059
+ JOB_ERROR,
1060
+ JOB_START,
1061
+ WORKFLOW_COMPLETE,
1062
+ WORKFLOW_ERROR,
1063
+ WORKFLOW_LOG,
1064
+ WORKFLOW_START
1065
+ } from "@openfn/engine-multi";
1066
+ var DEFAULT_BATCH_LIMIT = 10;
1067
+ var DEFAULT_BATCH_INTERVAL = 10;
1019
1068
  var eventMap = {
1020
- "workflow-start": RUN_START,
1021
- "job-start": STEP_START,
1022
- "job-complete": STEP_COMPLETE,
1023
- "workflow-log": RUN_LOG,
1024
- "workflow-complete": RUN_COMPLETE
1069
+ [WORKFLOW_START]: RUN_START,
1070
+ [JOB_START]: STEP_START,
1071
+ [JOB_COMPLETE]: STEP_COMPLETE,
1072
+ [WORKFLOW_LOG]: RUN_LOG,
1073
+ [WORKFLOW_COMPLETE]: RUN_COMPLETE
1025
1074
  };
1075
+ var allEngineEvents = [
1076
+ WORKFLOW_START,
1077
+ WORKFLOW_COMPLETE,
1078
+ JOB_START,
1079
+ JOB_COMPLETE,
1080
+ WORKFLOW_LOG,
1081
+ JOB_ERROR,
1082
+ WORKFLOW_ERROR
1083
+ ];
1084
+ function eventProcessor(engine, context, callbacks, options = {}) {
1085
+ const { id: planId, logger: logger2 } = context;
1086
+ const {
1087
+ batchLimit: limit = DEFAULT_BATCH_LIMIT,
1088
+ batchInterval: interval = DEFAULT_BATCH_INTERVAL
1089
+ } = options;
1090
+ const queue = [];
1091
+ const next = async () => {
1092
+ const evt = queue[0];
1093
+ if (evt) {
1094
+ await process2(evt.name, evt.event);
1095
+ queue.shift();
1096
+ next();
1097
+ }
1098
+ };
1099
+ let activeBatch = null;
1100
+ let batch = [];
1101
+ let start = -1;
1102
+ let batchTimeout;
1103
+ const sendBatch = async (name) => {
1104
+ clearTimeout(batchTimeout);
1105
+ activeBatch = null;
1106
+ await send(name, batch, batch.length);
1107
+ batch = [];
1108
+ };
1109
+ const send = async (name, payload, batchSize) => {
1110
+ const lightningEvent = eventMap[name] ?? name;
1111
+ await callbacks[name](context, payload);
1112
+ if (batchSize) {
1113
+ logger2.info(
1114
+ `${planId} :: sent ${lightningEvent} (${batchSize}):: OK :: ${Date.now() - start}ms`
1115
+ );
1116
+ } else {
1117
+ logger2.info(
1118
+ `${planId} :: sent ${lightningEvent} :: OK :: ${Date.now() - start}ms`
1119
+ );
1120
+ }
1121
+ };
1122
+ const process2 = async (name, event) => {
1123
+ if (name !== "workflow-log") {
1124
+ Sentry3.addBreadcrumb({
1125
+ category: "event",
1126
+ message: name,
1127
+ level: "info"
1128
+ });
1129
+ }
1130
+ if (name === activeBatch) {
1131
+ batch.push(event);
1132
+ if (batch.length >= limit) {
1133
+ await sendBatch(name);
1134
+ }
1135
+ return;
1136
+ } else if (activeBatch) {
1137
+ await sendBatch(activeBatch);
1138
+ }
1139
+ if (name in callbacks) {
1140
+ try {
1141
+ start = Date.now();
1142
+ if (options?.batch?.[name]) {
1143
+ activeBatch = name;
1144
+ batch.push(event);
1145
+ while (queue.length > 1 && queue[1].name === name) {
1146
+ const [nextBatchItem] = queue.splice(1, 1);
1147
+ batch.push(nextBatchItem.event);
1148
+ if (batch.length >= limit) {
1149
+ return sendBatch(name);
1150
+ }
1151
+ }
1152
+ if (!batchTimeout) {
1153
+ const batchName = activeBatch;
1154
+ batchTimeout = setTimeout(async () => {
1155
+ sendBatch(batchName);
1156
+ }, interval);
1157
+ }
1158
+ return;
1159
+ }
1160
+ await send(name, event);
1161
+ } catch (e2) {
1162
+ if (!e2.reportedToSentry) {
1163
+ Sentry3.captureException(e2);
1164
+ logger2.error(e2);
1165
+ }
1166
+ }
1167
+ } else {
1168
+ logger2.warn("no event bound for", name);
1169
+ }
1170
+ };
1171
+ const enqueue = (name, event) => {
1172
+ queue.push({ name, event });
1173
+ if (queue.length == 1) {
1174
+ next();
1175
+ }
1176
+ };
1177
+ const e = allEngineEvents.reduce(
1178
+ (obj, e2) => Object.assign(obj, { [e2]: (p) => enqueue(e2, p) }),
1179
+ {}
1180
+ );
1181
+ engine.listen(planId, e);
1182
+ }
1183
+
1184
+ // src/api/execute.ts
1185
+ var enc = new TextDecoder("utf-8");
1026
1186
  function execute(channel, engine, logger2, plan, input, options = {}, onFinish = (_result) => {
1027
1187
  }) {
1028
1188
  logger2.info("executing ", plan.id);
@@ -1036,8 +1196,8 @@ function execute(channel, engine, logger2, plan, input, options = {}, onFinish =
1036
1196
  options,
1037
1197
  onFinish
1038
1198
  };
1039
- Sentry3.withIsolationScope(async () => {
1040
- Sentry3.addBreadcrumb({
1199
+ Sentry4.withIsolationScope(async () => {
1200
+ Sentry4.addBreadcrumb({
1041
1201
  category: "run",
1042
1202
  message: "Executing run: loading metadata",
1043
1203
  level: "info",
@@ -1045,47 +1205,26 @@ function execute(channel, engine, logger2, plan, input, options = {}, onFinish =
1045
1205
  runId: plan.id
1046
1206
  }
1047
1207
  });
1048
- const throttle = throttle_default();
1049
- const addEvent = (eventName, handler) => {
1050
- const wrappedFn = async (event) => {
1051
- if (eventName !== "workflow-log") {
1052
- Sentry3.addBreadcrumb({
1053
- category: "event",
1054
- message: eventName,
1055
- level: "info"
1056
- });
1057
- }
1058
- const lightningEvent = eventMap[eventName] ?? eventName;
1059
- try {
1060
- let start = Date.now();
1061
- await handler(context, event);
1062
- logger2.info(
1063
- `${plan.id} :: sent ${lightningEvent} :: OK :: ${Date.now() - start}ms`
1064
- );
1065
- } catch (e) {
1066
- if (!e.reportedToSentry) {
1067
- Sentry3.captureException(e);
1068
- logger2.error(e);
1069
- }
1070
- }
1071
- };
1072
- return {
1073
- [eventName]: wrappedFn
1074
- };
1075
- };
1076
- const listeners = Object.assign(
1077
- {},
1078
- addEvent("workflow-start", throttle(onRunStart)),
1079
- addEvent("job-start", throttle(onStepStart)),
1080
- addEvent("job-complete", throttle(onStepComplete)),
1081
- addEvent("job-error", throttle(onJobError)),
1082
- addEvent("workflow-log", throttle(onJobLog)),
1083
- // This will also resolve the promise
1084
- addEvent("workflow-complete", throttle(onWorkflowComplete)),
1085
- addEvent("workflow-error", throttle(onRunError))
1086
- // TODO send autoinstall logs
1208
+ eventProcessor(
1209
+ engine,
1210
+ context,
1211
+ {
1212
+ [WORKFLOW_START2]: onRunStart,
1213
+ [JOB_START2]: onStepStart,
1214
+ [JOB_COMPLETE2]: onStepComplete,
1215
+ [JOB_ERROR2]: onJobError,
1216
+ [WORKFLOW_LOG2]: onRunLog,
1217
+ [WORKFLOW_COMPLETE2]: onWorkflowComplete,
1218
+ [WORKFLOW_ERROR2]: onRunError
1219
+ },
1220
+ {
1221
+ batch: options.batchLogs ? {
1222
+ [WORKFLOW_LOG2]: true
1223
+ } : {},
1224
+ batchInterval: options.batchInterval,
1225
+ batchLimit: options.batchLimit
1226
+ }
1087
1227
  );
1088
- engine.listen(plan.id, listeners);
1089
1228
  const resolvers = {
1090
1229
  credential: (id) => loadCredential(context, id)
1091
1230
  // TODO not supported right now
@@ -1108,7 +1247,7 @@ function execute(channel, engine, logger2, plan, input, options = {}, onFinish =
1108
1247
  }
1109
1248
  }
1110
1249
  try {
1111
- Sentry3.addBreadcrumb({
1250
+ Sentry4.addBreadcrumb({
1112
1251
  category: "run",
1113
1252
  message: "run metadata loaded: starting run",
1114
1253
  level: "info",
@@ -1118,7 +1257,7 @@ function execute(channel, engine, logger2, plan, input, options = {}, onFinish =
1118
1257
  });
1119
1258
  engine.execute(plan, loadedInput, { resolvers, ...options });
1120
1259
  } catch (e) {
1121
- Sentry3.addBreadcrumb({
1260
+ Sentry4.addBreadcrumb({
1122
1261
  category: "run",
1123
1262
  message: "exception in run",
1124
1263
  level: "info",
@@ -1145,29 +1284,6 @@ function onJobError(context, event) {
1145
1284
  return onStepComplete(context, event, event.error);
1146
1285
  }
1147
1286
  }
1148
- function onJobLog(context, event) {
1149
- const { state, options } = context;
1150
- let message = event.message;
1151
- if (event.redacted) {
1152
- message = [
1153
- `(Log message redacted: exceeds ${options.payloadLimitMb}mb memory limit)`
1154
- ];
1155
- } else if (typeof event.message === "string") {
1156
- message = JSON.parse(event.message);
1157
- }
1158
- const log = {
1159
- run_id: `${state.plan.id}`,
1160
- message,
1161
- source: event.name,
1162
- level: event.level,
1163
- // @ts-ignore
1164
- timestamp: timeInMicroseconds(event.time)
1165
- };
1166
- if (state.activeStep) {
1167
- log.step_id = state.activeStep;
1168
- }
1169
- return sendEvent(context, RUN_LOG, log);
1170
- }
1171
1287
  async function loadDataclip(context, stateId) {
1172
1288
  const result = await sendEvent(context, GET_DATACLIP, {
1173
1289
  id: stateId
@@ -1185,7 +1301,7 @@ var healthcheck_default = (ctx) => {
1185
1301
  };
1186
1302
 
1187
1303
  // src/channels/run.ts
1188
- import * as Sentry4 from "@sentry/node";
1304
+ import * as Sentry5 from "@sentry/node";
1189
1305
  var joinRunChannel = (socket, token, runId, logger2, timeout = 30) => {
1190
1306
  return new Promise((resolve5, reject) => {
1191
1307
  let didReceiveOk = false;
@@ -1204,12 +1320,12 @@ var joinRunChannel = (socket, token, runId, logger2, timeout = 30) => {
1204
1320
  resolve5({ channel, run: run2 });
1205
1321
  }
1206
1322
  }).receive("error", (err) => {
1207
- Sentry4.captureException(err);
1323
+ Sentry5.captureException(err);
1208
1324
  logger2.error(`error connecting to ${channelName}`, err);
1209
1325
  channel?.leave();
1210
1326
  reject(err);
1211
1327
  }).receive("timeout", (err) => {
1212
- Sentry4.captureException(err);
1328
+ Sentry5.captureException(err);
1213
1329
  logger2.error(`Timeout for ${channelName}`, err);
1214
1330
  channel?.leave();
1215
1331
  reject(err);
@@ -1226,7 +1342,7 @@ var run_default = joinRunChannel;
1226
1342
 
1227
1343
  // src/channels/worker-queue.ts
1228
1344
  import EventEmitter2 from "node:events";
1229
- import * as Sentry5 from "@sentry/node";
1345
+ import * as Sentry6 from "@sentry/node";
1230
1346
  import { Socket as PhxSocket } from "phoenix";
1231
1347
  import { WebSocket } from "ws";
1232
1348
  import { API_VERSION } from "@openfn/lexicon/lightning";
@@ -1265,13 +1381,13 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
1265
1381
  SocketConstructor = PhxSocket
1266
1382
  } = options;
1267
1383
  const events = new EventEmitter2();
1268
- Sentry5.addBreadcrumb({
1384
+ Sentry6.addBreadcrumb({
1269
1385
  category: "lifecycle",
1270
1386
  message: "Connecting to worker queue",
1271
1387
  level: "info"
1272
1388
  });
1273
1389
  worker_token_default(secret, serverId, logger2).then(async (token) => {
1274
- Sentry5.addBreadcrumb({
1390
+ Sentry6.addBreadcrumb({
1275
1391
  category: "lifecycle",
1276
1392
  message: "Worker token generated",
1277
1393
  level: "info"
@@ -1290,7 +1406,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
1290
1406
  let didOpen = false;
1291
1407
  let shouldReportConnectionError = true;
1292
1408
  socket.onOpen(() => {
1293
- Sentry5.addBreadcrumb({
1409
+ Sentry6.addBreadcrumb({
1294
1410
  category: "lifecycle",
1295
1411
  message: "Web socket connected",
1296
1412
  level: "info"
@@ -1317,7 +1433,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
1317
1433
  events.emit("disconnect");
1318
1434
  });
1319
1435
  socket.onError((e) => {
1320
- Sentry5.addBreadcrumb({
1436
+ Sentry6.addBreadcrumb({
1321
1437
  category: "lifecycle",
1322
1438
  message: "Error in web socket connection",
1323
1439
  level: "info"
@@ -1325,7 +1441,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
1325
1441
  if (shouldReportConnectionError) {
1326
1442
  logger2.debug("Reporting connection error to sentry");
1327
1443
  shouldReportConnectionError = false;
1328
- Sentry5.captureException(e);
1444
+ Sentry6.captureException(e);
1329
1445
  }
1330
1446
  if (!didOpen) {
1331
1447
  events.emit("error", e.message);
@@ -1435,11 +1551,11 @@ function createServer(engine, options = {}) {
1435
1551
  app.events = new EventEmitter3();
1436
1552
  app.engine = engine;
1437
1553
  if (options.sentryDsn) {
1438
- Sentry6.init({
1554
+ Sentry7.init({
1439
1555
  environment: options.sentryEnv,
1440
1556
  dsn: options.sentryDsn
1441
1557
  });
1442
- Sentry6.setupKoaErrorHandler(app);
1558
+ Sentry7.setupKoaErrorHandler(app);
1443
1559
  }
1444
1560
  app.use(bodyParser());
1445
1561
  app.use(
@@ -1496,8 +1612,14 @@ function createServer(engine, options = {}) {
1496
1612
  if (!("payloadLimitMb" in options2)) {
1497
1613
  options2.payloadLimitMb = app.options.payloadLimitMb;
1498
1614
  }
1615
+ if (!("logPayloadLimitMb" in options2)) {
1616
+ options2.logPayloadLimitMb = app.options.logPayloadLimitMb;
1617
+ }
1499
1618
  options2.timeoutRetryCount = app.options.timeoutRetryCount;
1500
1619
  options2.timeoutRetryDelay = app.options.timeoutRetryDelayMs ?? app.options.socketTimeoutSeconds;
1620
+ options2.batchLogs = app.options.batchLogs;
1621
+ options2.batchInterval = app.options.batchInterval;
1622
+ options2.batchLimit = app.options.batchLimit;
1501
1623
  const onFinish = () => {
1502
1624
  const duration = (Date.now() - start) / 1e3;
1503
1625
  logger2.debug(
@@ -6450,6 +6572,9 @@ function parseArgs(argv) {
6450
6572
  const {
6451
6573
  OPENFN_ADAPTORS_REPO,
6452
6574
  WORKER_BACKOFF,
6575
+ WORKER_BATCH_INTERVAL,
6576
+ WORKER_BATCH_LIMIT,
6577
+ WORKER_BATCH_LOGS,
6453
6578
  WORKER_CAPACITY,
6454
6579
  WORKER_CLAIM_TIMEOUT_SECONDS,
6455
6580
  WORKER_COLLECTIONS_URL,
@@ -6458,6 +6583,7 @@ function parseArgs(argv) {
6458
6583
  WORKER_LIGHTNING_SERVICE_URL,
6459
6584
  WORKER_LOG_LEVEL,
6460
6585
  WORKER_MAX_PAYLOAD_MB,
6586
+ WORKER_MAX_LOG_PAYLOAD_MB,
6461
6587
  WORKER_MAX_RUN_DURATION_SECONDS,
6462
6588
  WORKER_MAX_RUN_MEMORY_MB,
6463
6589
  WORKER_MESSAGE_TIMEOUT_SECONDS,
@@ -6510,6 +6636,9 @@ function parseArgs(argv) {
6510
6636
  description: "Base64-encoded public key. Used to verify run tokens. Env: WORKER_LIGHTNING_PUBLIC_KEY"
6511
6637
  }).option("log", {
6512
6638
  description: "Set the log level for stdout (default to info, set to debug for verbose output). Env: WORKER_LOG_LEVEL"
6639
+ }).option("log-payload-memory", {
6640
+ description: "Maximum memory allocated to log payloads, in mb. Env: WORKER_MAX_LOG_PAYLOAD_MB",
6641
+ type: "number"
6513
6642
  }).option("loop", {
6514
6643
  description: "Disable the claims loop",
6515
6644
  type: "boolean",
@@ -6518,6 +6647,15 @@ function parseArgs(argv) {
6518
6647
  description: "Use a mock runtime engine",
6519
6648
  type: "boolean",
6520
6649
  default: false
6650
+ }).option("batch-logs", {
6651
+ description: "Allow logs emitted from the server to be batched up. Env: WORKER_BATCH_LOGS",
6652
+ type: "boolean"
6653
+ }).option("batch-interval", {
6654
+ description: "Interval for batching logs, in milliseconds. Env: WORKER_BATCH_INTERVAL",
6655
+ type: "number"
6656
+ }).option("batch-limit", {
6657
+ description: "Maximum number of logs to batch before sending. Env: WORKER_BATCH_LIMIT",
6658
+ type: "number"
6521
6659
  }).option("backoff", {
6522
6660
  description: "Claim backoff rules: min/max (in seconds). Env: WORKER_BACKOFF"
6523
6661
  }).option("capacity", {
@@ -6571,6 +6709,9 @@ function parseArgs(argv) {
6571
6709
  "ws://localhost:4000/worker"
6572
6710
  ),
6573
6711
  repoDir: setArg(args2.repoDir, WORKER_REPO_DIR),
6712
+ batchLogs: setArg(args2.batchLogs, WORKER_BATCH_LOGS, false),
6713
+ batchInterval: setArg(args2.batchInterval, WORKER_BATCH_INTERVAL, 10),
6714
+ batchLimit: setArg(args2.batchLimit, WORKER_BATCH_LIMIT, 50),
6574
6715
  monorepoDir: setArg(args2.monorepoDir, OPENFN_ADAPTORS_REPO),
6575
6716
  secret: setArg(args2.secret, WORKER_SECRET),
6576
6717
  sentryDsn: setArg(args2.sentryDsn, WORKER_SENTRY_DSN),
@@ -6589,6 +6730,11 @@ function parseArgs(argv) {
6589
6730
  ),
6590
6731
  runMemory: setArg(args2.runMemory, WORKER_MAX_RUN_MEMORY_MB, 500),
6591
6732
  payloadMemory: setArg(args2.payloadMemory, WORKER_MAX_PAYLOAD_MB, 10),
6733
+ logPayloadMemory: setArg(
6734
+ args2.logPayloadMemory,
6735
+ WORKER_MAX_LOG_PAYLOAD_MB,
6736
+ 1
6737
+ ),
6592
6738
  maxRunDurationSeconds: setArg(
6593
6739
  args2.maxRunDurationSeconds,
6594
6740
  WORKER_MAX_RUN_DURATION_SECONDS,
@@ -6672,6 +6818,8 @@ function engineReady(engine) {
6672
6818
  },
6673
6819
  maxWorkflows: args.capacity,
6674
6820
  payloadLimitMb: args.payloadMemory,
6821
+ logPayloadLimitMb: args.logPayloadMemory ?? 1,
6822
+ // Default to 1MB
6675
6823
  collectionsVersion: args.collectionsVersion,
6676
6824
  collectionsUrl: args.collectionsUrl,
6677
6825
  monorepoDir: args.monorepoDir,
@@ -6680,7 +6828,10 @@ function engineReady(engine) {
6680
6828
  // deprecated!
6681
6829
  socketTimeoutSeconds: args.socketTimeoutSeconds,
6682
6830
  timeoutRetryCount: args.timeoutRetryCount,
6683
- timeoutRetryDelayMs: args.timeoutRetryDelayMs
6831
+ timeoutRetryDelayMs: args.timeoutRetryDelayMs,
6832
+ batchLogs: args.batchLogs,
6833
+ batchInterval: args.batchInterval,
6834
+ batchLimit: args.batchLimit
6684
6835
  };
6685
6836
  if (args.socketTimeoutSeconds) {
6686
6837
  logger.warn(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfn/ws-worker",
3
- "version": "1.19.7",
3
+ "version": "1.20.1",
4
4
  "description": "A Websocket Worker to connect Lightning to a Runtime Engine",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -23,10 +23,10 @@
23
23
  "koa-logger": "^3.2.1",
24
24
  "phoenix": "1.7.10",
25
25
  "ws": "^8.18.3",
26
- "@openfn/engine-multi": "1.8.5",
27
- "@openfn/logger": "1.1.0",
28
- "@openfn/runtime": "1.7.6",
29
- "@openfn/lexicon": "^1.2.7"
26
+ "@openfn/engine-multi": "1.9.1",
27
+ "@openfn/lexicon": "^1.3.0",
28
+ "@openfn/logger": "1.1.1",
29
+ "@openfn/runtime": "1.7.7"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/koa": "^2.15.0",
@@ -43,7 +43,7 @@
43
43
  "tsup": "^6.7.0",
44
44
  "typescript": "^4.9.5",
45
45
  "yargs": "^17.7.2",
46
- "@openfn/lightning-mock": "2.3.8"
46
+ "@openfn/lightning-mock": "2.3.10"
47
47
  },
48
48
  "files": [
49
49
  "dist",