@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.bf95a6f.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.
Files changed (99) hide show
  1. package/package.json +13 -7
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +7 -3
  5. package/src/errors.d.ts +12 -2
  6. package/src/errors.js +17 -3
  7. package/src/errors.spec.js +19 -0
  8. package/src/helpers/activity_options.js +21 -0
  9. package/src/helpers/activity_options.spec.js +55 -0
  10. package/src/helpers/error_serializer.js +236 -0
  11. package/src/helpers/error_serializer.spec.js +413 -0
  12. package/src/helpers/errors.js +23 -14
  13. package/src/helpers/errors.spec.js +31 -75
  14. package/src/helpers/fetch.js +1 -38
  15. package/src/helpers/fetch.spec.js +1 -39
  16. package/src/helpers/function.js +13 -0
  17. package/src/helpers/function.spec.js +23 -1
  18. package/src/helpers/object.js +48 -35
  19. package/src/helpers/object.spec.js +123 -75
  20. package/src/helpers/redact.js +51 -0
  21. package/src/helpers/redact.spec.js +66 -0
  22. package/src/helpers/string.js +10 -0
  23. package/src/helpers/string.spec.js +36 -1
  24. package/src/hooks/index.d.ts +76 -78
  25. package/src/hooks/index.js +57 -49
  26. package/src/hooks/index.spec.js +104 -105
  27. package/src/hooks/pending_hooks.js +24 -0
  28. package/src/hooks/pending_hooks.spec.js +62 -0
  29. package/src/interface/evaluator.d.ts +10 -9
  30. package/src/interface/evaluator.js +1 -6
  31. package/src/interface/evaluator.spec.js +10 -10
  32. package/src/interface/index.js +2 -1
  33. package/src/interface/step.d.ts +16 -15
  34. package/src/interface/step.js +1 -6
  35. package/src/interface/step.spec.js +10 -10
  36. package/src/interface/validations/index.js +33 -53
  37. package/src/interface/validations/index.spec.js +25 -17
  38. package/src/interface/webhook.js +1 -2
  39. package/src/interface/webhook.spec.js +25 -19
  40. package/src/interface/workflow.d.ts +25 -18
  41. package/src/interface/workflow.js +64 -85
  42. package/src/interface/workflow.spec.js +220 -226
  43. package/src/interface/workflow_activity_options.js +2 -5
  44. package/src/interface/workflow_utils.d.ts +15 -0
  45. package/src/interface/workflow_utils.js +19 -0
  46. package/src/interface/workflow_utils.spec.js +82 -1
  47. package/src/sdk/helpers/objects.d.ts +24 -19
  48. package/src/sdk/runtime/events.d.ts +4 -6
  49. package/src/sdk/runtime/events.js +2 -15
  50. package/src/sdk/runtime/events.spec.js +14 -92
  51. package/src/temporal/client.js +47 -0
  52. package/src/temporal/client.spec.js +124 -0
  53. package/src/temporal/index.d.ts +32 -0
  54. package/src/temporal/index.js +1 -0
  55. package/src/tracing/trace_engine.js +9 -11
  56. package/src/tracing/trace_engine.spec.js +67 -28
  57. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  58. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  59. package/src/worker/configs.js +0 -3
  60. package/src/worker/global_functions.js +2 -2
  61. package/src/worker/global_functions.spec.js +3 -3
  62. package/src/worker/index.js +24 -11
  63. package/src/worker/index.spec.js +36 -16
  64. package/src/worker/interceptors/activity.js +34 -21
  65. package/src/worker/interceptors/activity.spec.js +77 -172
  66. package/src/worker/interceptors/workflow.js +58 -16
  67. package/src/worker/interceptors/workflow.spec.js +183 -56
  68. package/src/worker/loader/activities.js +56 -40
  69. package/src/worker/loader/activities.spec.js +21 -22
  70. package/src/worker/loader/workflows.js +16 -3
  71. package/src/worker/loader/workflows.spec.js +36 -5
  72. package/src/worker/log_hooks.js +22 -19
  73. package/src/worker/log_hooks.spec.js +14 -6
  74. package/src/worker/sinks.js +8 -8
  75. package/src/worker/sinks.spec.js +9 -9
  76. package/src/worker/telemetry.js +2 -1
  77. package/src/worker/temporal_logger.js +33 -0
  78. package/src/worker/temporal_logger.spec.js +90 -0
  79. package/src/worker/webpack_loaders/consts.js +6 -0
  80. package/src/worker/webpack_loaders/tools.js +1 -146
  81. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  82. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  83. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  84. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  85. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  86. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  87. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  88. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  89. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  90. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  91. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  92. package/src/helpers/aggregations.js +0 -44
  93. package/src/helpers/aggregations.spec.js +0 -92
  94. package/src/tracing/tools/utils.js +0 -42
  95. package/src/tracing/tools/utils.spec.js +0 -83
  96. package/src/worker/proxy.js +0 -20
  97. package/src/worker/proxy.spec.js +0 -51
  98. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  99. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -1,7 +1,7 @@
1
1
  import { EOL } from 'node:os';
2
2
  import { writeFileInTempDir, findWorkflowsInNodeModules, importComponents, matchFiles } from './tools.js';
3
3
  import { staticMatchers } from './matchers.js';
4
- import { WORKFLOWS_INDEX_FILENAME, WORKFLOW_CATALOG } from '#consts';
4
+ import { WORKFLOWS_INDEX_FILENAME, WORKFLOW_CATALOG, WORKFLOW_OPTIONS_FILENAME } from '#consts';
5
5
  import { createChildLogger } from '#logger';
6
6
  import { ValidationError } from '#errors';
7
7
 
@@ -38,6 +38,7 @@ const createWorkflowsEntrypoint = workflows => {
38
38
  * @typedef LoadWorkflowsResult
39
39
  * @property {Workflow[]} workflows - Loaded workflows
40
40
  * @property {string} entrypoint - Index file loading all workflows
41
+ * @property {string} optionsFile - File exporting options for each workflow and their aliases
41
42
  */
42
43
  /**
43
44
  * Scan and find workflow.js files and import them.
@@ -47,6 +48,8 @@ const createWorkflowsEntrypoint = workflows => {
47
48
  */
48
49
  export async function loadWorkflows( rootDir ) {
49
50
  const workflowNames = new Set();
51
+ const workflowOptions = new Map();
52
+
50
53
  const workflows = [];
51
54
  const localWorkflows = matchFiles( rootDir, [ staticMatchers.workflowFile ] );
52
55
  const externalWorkflows = findWorkflowsInNodeModules( rootDir );
@@ -55,7 +58,7 @@ export async function loadWorkflows( rootDir ) {
55
58
  if ( staticMatchers.workflowPathHasShared( path ) ) {
56
59
  throw new ValidationError( 'Workflow directory can\'t be named "shared"' );
57
60
  }
58
- const { name, aliases } = metadata;
61
+ const { name, aliases, options } = metadata;
59
62
  if ( workflowNames.has( name ) ) {
60
63
  throw new ValidationError( `Workflow name "${name}" conflicts with another workflow or alias. \
61
64
  Workflow names and aliases must be unique.` );
@@ -64,6 +67,8 @@ Workflow names and aliases must be unique.` );
64
67
  throw new ValidationError( `Workflow name "${name}" is reserved for the internal catalog workflow.` );
65
68
  }
66
69
  workflowNames.add( name );
70
+ workflowOptions.set( name, { disableTrace: options?.disableTrace ?? false } );
71
+
67
72
  for ( const alias of aliases ?? [] ) {
68
73
  if ( workflowNames.has( alias ) ) {
69
74
  throw new ValidationError( `Workflow "${name}" alias "${alias}" conflicts with another workflow or alias. \
@@ -73,10 +78,18 @@ Workflow names and aliases must be unique.` );
73
78
  throw new ValidationError( `Workflow "${name}" alias "${alias}" is reserved for the internal catalog workflow.` );
74
79
  }
75
80
  workflowNames.add( alias );
81
+ workflowOptions.set( alias, { disableTrace: options?.disableTrace ?? false } );
76
82
  }
77
83
 
78
84
  log.info( name, { path, aliases, ...( external && { external } ) } );
79
85
  workflows.push( { ...metadata, path, external } );
80
86
  }
81
- return { workflows, entrypoint: createWorkflowsEntrypoint( workflows ) };
87
+
88
+ workflowOptions.set( WORKFLOW_CATALOG, { disableTrace: true } );
89
+
90
+ // writes down the static workflow options, currently only disable trace
91
+ const optionsContent = `export default ${JSON.stringify( Object.fromEntries( workflowOptions ), undefined, 2 )};`;
92
+ const optionsFile = writeFileInTempDir( optionsContent, WORKFLOW_OPTIONS_FILENAME );
93
+
94
+ return { workflows, entrypoint: createWorkflowsEntrypoint( workflows ), optionsFile };
82
95
  };
@@ -6,7 +6,7 @@ vi.mock( '#consts', () => ( {
6
6
  WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
7
7
  WORKFLOW_CATALOG: 'catalog',
8
8
  ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
9
- SHARED_STEP_PREFIX: '$shared'
9
+ WORKFLOW_OPTIONS_FILENAME: '__workflow_options.js'
10
10
  } ) );
11
11
 
12
12
  const { importComponentsMock, findWorkflowsInNodeModulesMock, matchFilesMock, writeFileInTempDirMock } = vi.hoisted( () => ( {
@@ -45,7 +45,7 @@ describe( 'loadWorkflows', () => {
45
45
  matchFilesMock.mockReset();
46
46
  matchFilesMock.mockReturnValue( [] );
47
47
  writeFileInTempDirMock.mockReset();
48
- writeFileInTempDirMock.mockReturnValue( '/tmp/__workflows_entrypoint.js' );
48
+ writeFileInTempDirMock.mockImplementation( ( _, filename ) => `/tmp/${filename}` );
49
49
  workflowPathHasSharedMock.mockReset();
50
50
  workflowPathHasSharedMock.mockReturnValue( false );
51
51
  } );
@@ -59,9 +59,10 @@ describe( 'loadWorkflows', () => {
59
59
  yield { metadata: { name: 'Flow1', description: 'd' }, path: '/b/workflow.js' };
60
60
  } );
61
61
 
62
- const { workflows, entrypoint } = await loadWorkflows( '/root' );
62
+ const { workflows, entrypoint, optionsFile } = await loadWorkflows( '/root' );
63
63
  expect( workflows ).toEqual( [ { name: 'Flow1', description: 'd', path: '/b/workflow.js', external: false } ] );
64
64
  expect( entrypoint ).toBe( '/tmp/__workflows_entrypoint.js' );
65
+ expect( optionsFile ).toBe( '/tmp/__workflow_options.js' );
65
66
  expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, localFiles );
66
67
  expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledOnce();
67
68
  expect( findWorkflowsInNodeModulesMock ).toHaveBeenCalledWith( '/root' );
@@ -245,12 +246,42 @@ describe( 'loadWorkflows', () => {
245
246
  const { entrypoint } = await loadWorkflows( '/root' );
246
247
 
247
248
  expect( entrypoint ).toBe( '/tmp/__workflows_entrypoint.js' );
248
- expect( writeFileInTempDirMock ).toHaveBeenCalledTimes( 1 );
249
- const [ contents, filename ] = writeFileInTempDirMock.mock.calls[0];
249
+ expect( writeFileInTempDirMock ).toHaveBeenCalledTimes( 2 );
250
+ const [ contents, filename ] = writeFileInTempDirMock.mock.calls.find( ( [ , calledFilename ] ) =>
251
+ calledFilename === '__workflows_entrypoint.js'
252
+ );
250
253
  expect( filename ).toBe( '__workflows_entrypoint.js' );
251
254
  expect( contents ).toContain( 'export { default as W } from \'/abs/wf.js\';' );
252
255
  expect( contents ).toContain( 'export { default as W_old } from \'/abs/wf.js\';' );
253
256
  expect( contents ).toContain( 'export { default as W_legacy } from \'/abs/wf.js\';' );
254
257
  expect( contents ).toContain( 'export { default as catalog }' );
255
258
  } );
259
+
260
+ it( 'writes trace options for workflow names, aliases, and the catalog workflow', async () => {
261
+ const { loadWorkflows } = await import( './workflows.js' );
262
+ importComponentsMock.mockImplementationOnce( async function *() {
263
+ yield {
264
+ metadata: { name: 'Traced', aliases: [ 'Traced_old' ] },
265
+ path: '/abs/traced.js'
266
+ };
267
+ yield {
268
+ metadata: { name: 'Untraced', aliases: [ 'Untraced_old' ], options: { disableTrace: true } },
269
+ path: '/abs/untraced.js'
270
+ };
271
+ } );
272
+
273
+ await loadWorkflows( '/root' );
274
+
275
+ const [ contents, filename ] = writeFileInTempDirMock.mock.calls.find( ( [ , calledFilename ] ) =>
276
+ calledFilename === '__workflow_options.js'
277
+ );
278
+ expect( filename ).toBe( '__workflow_options.js' );
279
+ expect( contents ).toBe( `export default ${JSON.stringify( {
280
+ Traced: { disableTrace: false },
281
+ Traced_old: { disableTrace: false },
282
+ Untraced: { disableTrace: true },
283
+ Untraced_old: { disableTrace: true },
284
+ catalog: { disableTrace: true }
285
+ }, undefined, 2 )};` );
286
+ } );
256
287
  } );
@@ -1,6 +1,7 @@
1
- import { messageBus } from '#bus';
1
+ import { mainEventBus } from '#bus';
2
2
  import { createChildLogger } from '#logger';
3
3
  import { ACTIVITY_GET_TRACE_DESTINATIONS, BusEventType, LifecycleEvent, WORKFLOW_CATALOG } from '#consts';
4
+ import { serializeError } from '#helpers/error_serializer';
4
5
 
5
6
  const activityLog = createChildLogger( 'Activity' );
6
7
  const workflowLog = createChildLogger( 'Workflow' );
@@ -25,29 +26,30 @@ const serializedActivityFields = activityInfo => ( {
25
26
 
26
27
  const shouldLogActivity = activityInfo => activityInfo.activityType !== ACTIVITY_GET_TRACE_DESTINATIONS;
27
28
 
28
- messageBus.on( BusEventType.ACTIVITY_START, ( { activityInfo, outputActivityKind } ) =>
29
+ mainEventBus.on( BusEventType.ACTIVITY_START, ( { activityInfo, outputActivityKind } ) =>
29
30
  shouldLogActivity( activityInfo ) && activityLog.info( `Started ${activityInfo.activityType} ${outputActivityKind}`, {
30
31
  event: LifecycleEvent.START,
31
32
  ...serializedActivityFields( activityInfo )
32
33
  } )
33
34
  );
34
35
 
35
- messageBus.on( BusEventType.ACTIVITY_END, ( { activityInfo, outputActivityKind } ) =>
36
+ mainEventBus.on( BusEventType.ACTIVITY_END, ( { activityInfo, outputActivityKind } ) =>
36
37
  shouldLogActivity( activityInfo ) && activityLog.info( `Ended ${activityInfo.activityType} ${outputActivityKind}`, {
37
38
  event: LifecycleEvent.END,
38
39
  ...serializedActivityFields( activityInfo )
39
40
  } )
40
41
  );
41
42
 
42
- messageBus.on( BusEventType.ACTIVITY_ERROR, ( { activityInfo, outputActivityKind, error } ) =>
43
- shouldLogActivity( activityInfo ) && activityLog.error( `Error ${activityInfo.activityType} ${outputActivityKind}: ${error.constructor.name}`, {
44
- event: LifecycleEvent.ERROR,
45
- ...serializedActivityFields( activityInfo ),
46
- error: error.message
47
- } )
43
+ mainEventBus.on( BusEventType.ACTIVITY_ERROR, ( { activityInfo, outputActivityKind, error } ) =>
44
+ shouldLogActivity( activityInfo ) &&
45
+ activityLog.error( `Error ${activityInfo.activityType} ${outputActivityKind}: ${error.name ?? error.constructor.name}`, {
46
+ event: LifecycleEvent.ERROR,
47
+ ...serializedActivityFields( activityInfo ),
48
+ error: serializeError( error, { dropKeys: [ 'stack' ] } )
49
+ } )
48
50
  );
49
51
 
50
- messageBus.on( BusEventType.ACTIVITY_LOG, ( { level, message, metadata, activityInfo } ) =>
52
+ mainEventBus.on( BusEventType.ACTIVITY_LOG, ( { level, message, metadata, activityInfo } ) =>
51
53
  activityLog[level]( message, {
52
54
  ...metadata ?? {},
53
55
  ...serializedActivityFields( activityInfo )
@@ -68,29 +70,30 @@ const serializeWorkflowFields = workflowDetails => ( {
68
70
 
69
71
  const shouldLogWorkflow = workflowDetails => workflowDetails.workflowType !== WORKFLOW_CATALOG;
70
72
 
71
- messageBus.on( BusEventType.WORKFLOW_START, ( { workflowDetails } ) =>
73
+ mainEventBus.on( BusEventType.WORKFLOW_START, ( { workflowDetails } ) =>
72
74
  shouldLogWorkflow( workflowDetails ) && workflowLog.info( `Started ${workflowDetails.workflowType} workflow`, {
73
75
  event: LifecycleEvent.START,
74
76
  ...serializeWorkflowFields( workflowDetails )
75
77
  } )
76
78
  );
77
79
 
78
- messageBus.on( BusEventType.WORKFLOW_END, ( { workflowDetails } ) =>
80
+ mainEventBus.on( BusEventType.WORKFLOW_END, ( { workflowDetails } ) =>
79
81
  shouldLogWorkflow( workflowDetails ) && workflowLog.info( `Ended ${workflowDetails.workflowType} workflow`, {
80
82
  event: LifecycleEvent.END,
81
83
  ...serializeWorkflowFields( workflowDetails )
82
84
  } )
83
85
  );
84
86
 
85
- messageBus.on( BusEventType.WORKFLOW_ERROR, ( { workflowDetails, error } ) =>
86
- shouldLogWorkflow( workflowDetails ) && workflowLog.error( `Error ${workflowDetails.workflowType} workflow: ${error.constructor.name}`, {
87
- event: LifecycleEvent.ERROR,
88
- ...serializeWorkflowFields( workflowDetails ),
89
- error: error.message
90
- } )
87
+ mainEventBus.on( BusEventType.WORKFLOW_ERROR, ( { workflowDetails, error } ) =>
88
+ shouldLogWorkflow( workflowDetails ) &&
89
+ workflowLog.error( `Error ${workflowDetails.workflowType} workflow: ${error.name ?? error.constructor.name}`, {
90
+ event: LifecycleEvent.ERROR,
91
+ ...serializeWorkflowFields( workflowDetails ),
92
+ error: serializeError( error, { dropKeys: [ 'stack' ] } )
93
+ } )
91
94
  );
92
95
 
93
- messageBus.on( BusEventType.WORKFLOW_LOG, ( { level, message, metadata, workflowDetails } ) =>
96
+ mainEventBus.on( BusEventType.WORKFLOW_LOG, ( { level, message, metadata, workflowDetails } ) =>
94
97
  workflowLog[level]( message, {
95
98
  ...metadata ?? {},
96
99
  ...serializeWorkflowFields( workflowDetails )
@@ -22,14 +22,19 @@ const createChildLoggerMock = vi.hoisted( () =>
22
22
  );
23
23
 
24
24
  const onHandlers = vi.hoisted( () => ( {} ) );
25
- const messageBusMock = vi.hoisted( () => ( {
25
+ const mainEventBusMock = vi.hoisted( () => ( {
26
26
  on: vi.fn( ( eventType, handler ) => {
27
27
  onHandlers[eventType] = handler;
28
28
  } )
29
29
  } ) );
30
+ const serializeErrorMock = vi.hoisted( () => vi.fn( error => ( {
31
+ name: error.name,
32
+ message: error.message
33
+ } ) ) );
30
34
 
31
35
  vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
32
- vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
36
+ vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
37
+ vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
33
38
 
34
39
  import './log_hooks.js';
35
40
 
@@ -104,6 +109,7 @@ describe( 'log_hooks', () => {
104
109
 
105
110
  it( 'ACTIVITY_ERROR logs full message and second arg', () => {
106
111
  const err = new Error( 'step failed' );
112
+ err.name = 'ProviderError';
107
113
  onHandlers[BusEventType.ACTIVITY_ERROR]( {
108
114
  ...basePayload,
109
115
  duration: 100,
@@ -112,7 +118,7 @@ describe( 'log_hooks', () => {
112
118
 
113
119
  expect( activityLogMock.error ).toHaveBeenCalledTimes( 1 );
114
120
  expect( activityLogMock.error ).toHaveBeenCalledWith(
115
- 'Error myWorkflow#myStep step: Error',
121
+ 'Error myWorkflow#myStep step: ProviderError',
116
122
  {
117
123
  event: LifecycleEvent.ERROR,
118
124
  activityId: 'act-1',
@@ -120,9 +126,10 @@ describe( 'log_hooks', () => {
120
126
  workflowId: 'wf-1',
121
127
  workflowType: 'myWorkflow',
122
128
  runId: 'run-1',
123
- error: 'step failed'
129
+ error: { name: 'ProviderError', message: 'step failed' }
124
130
  }
125
131
  );
132
+ expect( serializeErrorMock ).toHaveBeenCalledWith( err, { dropKeys: [ 'stack' ] } );
126
133
  } );
127
134
 
128
135
  it( 'ACTIVITY_ERROR does not log trace destination activity', () => {
@@ -229,7 +236,7 @@ describe( 'log_hooks', () => {
229
236
  } );
230
237
 
231
238
  it( 'WORKFLOW_ERROR logs full message and second arg', () => {
232
- const err = new TypeError( 'workflow boom' );
239
+ const err = { name: 'TypeError', message: 'workflow boom' };
233
240
  onHandlers[BusEventType.WORKFLOW_ERROR]( {
234
241
  ...basePayload,
235
242
  duration: 150,
@@ -244,9 +251,10 @@ describe( 'log_hooks', () => {
244
251
  workflowId: 'wf-1',
245
252
  workflowType: 'myWorkflow',
246
253
  runId: 'run-1',
247
- error: 'workflow boom'
254
+ error: { name: 'TypeError', message: 'workflow boom' }
248
255
  }
249
256
  );
257
+ expect( serializeErrorMock ).toHaveBeenCalledWith( err, { dropKeys: [ 'stack' ] } );
250
258
  } );
251
259
 
252
260
  it( 'WORKFLOW_ERROR does not log when workflowType is WORKFLOW_CATALOG', () => {
@@ -1,6 +1,6 @@
1
1
  import { BusEventType, ComponentType } from '#consts';
2
2
  import * as Tracing from '#tracing';
3
- import { messageBus } from '#bus';
3
+ import { mainEventBus } from '#bus';
4
4
  import { createWorkflowDetails } from '#helpers/temporal_context';
5
5
 
6
6
  // This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
@@ -12,15 +12,15 @@ export const sinks = {
12
12
  workflow: {
13
13
  log: {
14
14
  fn: ( workflowInfo, { level, message, metadata } ) => {
15
- messageBus.emit( BusEventType.WORKFLOW_LOG, { level, message, metadata, workflowDetails: createWorkflowDetails( workflowInfo ) } );
15
+ mainEventBus.emit( BusEventType.WORKFLOW_LOG, { level, message, metadata, workflowDetails: createWorkflowDetails( workflowInfo ) } );
16
16
  },
17
17
  callDuringReplay: false
18
18
  },
19
19
  start: {
20
20
  fn: ( workflowInfo, input ) => {
21
21
  const { runId, workflowType, memo: { traceInfo }, parent } = workflowInfo;
22
- messageBus.emit( BusEventType.WORKFLOW_START, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
23
- if ( traceInfo ) { // internal workflows (catalog) do not have this info
22
+ mainEventBus.emit( BusEventType.WORKFLOW_START, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
23
+ if ( traceInfo ) {
24
24
  Tracing.addEventStart( {
25
25
  id: runId,
26
26
  kind: ComponentType.WORKFLOW,
@@ -37,8 +37,8 @@ export const sinks = {
37
37
  end: {
38
38
  fn: ( workflowInfo, output ) => {
39
39
  const { runId, memo: { traceInfo } } = workflowInfo;
40
- messageBus.emit( BusEventType.WORKFLOW_END, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
41
- if ( traceInfo ) { // internal workflows (catalog) do not have this info
40
+ mainEventBus.emit( BusEventType.WORKFLOW_END, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
41
+ if ( traceInfo ) {
42
42
  Tracing.addEventEnd( { id: runId, details: output, traceInfo } );
43
43
  }
44
44
  },
@@ -48,8 +48,8 @@ export const sinks = {
48
48
  error: {
49
49
  fn: ( workflowInfo, error ) => {
50
50
  const { runId, memo: { traceInfo } } = workflowInfo;
51
- messageBus.emit( BusEventType.WORKFLOW_ERROR, { workflowDetails: createWorkflowDetails( workflowInfo ), error } );
52
- if ( traceInfo ) { // internal workflows (catalog) do not have this info
51
+ mainEventBus.emit( BusEventType.WORKFLOW_ERROR, { workflowDetails: createWorkflowDetails( workflowInfo ), error } );
52
+ if ( traceInfo ) {
53
53
  Tracing.addEventError( { id: runId, details: error, traceInfo } );
54
54
  }
55
55
  },
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { BusEventType, ComponentType } from '#consts';
3
3
 
4
- const messageBusMock = vi.hoisted( () => ( {
4
+ const mainEventBusMock = vi.hoisted( () => ( {
5
5
  emit: vi.fn()
6
6
  } ) );
7
7
 
@@ -15,7 +15,7 @@ const createWorkflowDetailsMock = vi.hoisted( () => vi.fn( workflowInfo => ( {
15
15
  runId: workflowInfo.runId
16
16
  } ) ) );
17
17
 
18
- vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
18
+ vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
19
19
  vi.mock( '#tracing', () => ( {
20
20
  addEventStart: addEventStartMock,
21
21
  addEventEnd: addEventEndMock,
@@ -69,7 +69,7 @@ describe( 'worker/sinks', () => {
69
69
  } );
70
70
 
71
71
  expect( createWorkflowDetailsMock ).toHaveBeenCalledWith( workflowInfo );
72
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_LOG, {
72
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_LOG, {
73
73
  level: 'info',
74
74
  message: 'workflow detail',
75
75
  metadata,
@@ -83,7 +83,7 @@ describe( 'worker/sinks', () => {
83
83
 
84
84
  sinks.workflow.start.fn( workflowInfo, input );
85
85
 
86
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
86
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
87
87
  expect( addEventStartMock ).toHaveBeenCalledWith( {
88
88
  id: 'run-1',
89
89
  kind: ComponentType.WORKFLOW,
@@ -99,7 +99,7 @@ describe( 'worker/sinks', () => {
99
99
 
100
100
  sinks.workflow.start.fn( { ...workflowInfo, memo: {} }, { value: 'input' } );
101
101
 
102
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
102
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
103
103
  expect( addEventStartMock ).not.toHaveBeenCalled();
104
104
  } );
105
105
 
@@ -109,7 +109,7 @@ describe( 'worker/sinks', () => {
109
109
 
110
110
  sinks.workflow.end.fn( workflowInfo, output );
111
111
 
112
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
112
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
113
113
  expect( addEventEndMock ).toHaveBeenCalledWith( {
114
114
  id: 'run-1',
115
115
  details: output,
@@ -122,7 +122,7 @@ describe( 'worker/sinks', () => {
122
122
 
123
123
  sinks.workflow.end.fn( { ...workflowInfo, memo: {} }, { value: 'output' } );
124
124
 
125
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
125
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
126
126
  expect( addEventEndMock ).not.toHaveBeenCalled();
127
127
  } );
128
128
 
@@ -132,7 +132,7 @@ describe( 'worker/sinks', () => {
132
132
 
133
133
  sinks.workflow.error.fn( workflowInfo, error );
134
134
 
135
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
135
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
136
136
  expect( addEventErrorMock ).toHaveBeenCalledWith( {
137
137
  id: 'run-1',
138
138
  details: error,
@@ -146,7 +146,7 @@ describe( 'worker/sinks', () => {
146
146
 
147
147
  sinks.workflow.error.fn( { ...workflowInfo, memo: {} }, error );
148
148
 
149
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
149
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
150
150
  expect( addEventErrorMock ).not.toHaveBeenCalled();
151
151
  } );
152
152
 
@@ -1,3 +1,4 @@
1
+ import { serializeError } from '#helpers/error_serializer';
1
2
  import { createChildLogger } from '#logger';
2
3
  import { workerTelemetryIntervalMs } from './configs.js';
3
4
 
@@ -18,7 +19,7 @@ export const setupTelemetry = ( { worker } ) => {
18
19
  }
19
20
  } );
20
21
  } catch ( error ) {
21
- log.warn( 'Failure', { error: error.message } );
22
+ log.warn( 'Failure', { error: serializeError( error, { dropKeys: [ 'stack' ] } ) } );
22
23
  }
23
24
  }, workerTelemetryIntervalMs ).unref();
24
25
  };
@@ -0,0 +1,33 @@
1
+ import { DefaultLogger, makeTelemetryFilterString, Runtime } from '@temporalio/worker';
2
+ import { createChildLogger } from '#logger';
3
+
4
+ const log = createChildLogger( 'Temporal' );
5
+ const OMITTED_MESSAGES = [ 'Activity failed', 'Workflow failed' ];
6
+
7
+ const temporalLevelToInternalLevelMap = {
8
+ TRACE: 'debug',
9
+ DEBUG: 'debug',
10
+ INFO: 'info',
11
+ WARN: 'warn',
12
+ ERROR: 'error'
13
+ };
14
+
15
+ export const setupTemporalLogger = () => {
16
+ const level = process.env.OUTPUT_TEMPORAL_LOG_LEVEL?.toUpperCase() ?? 'INFO';
17
+
18
+ Runtime.install( {
19
+ logger: new DefaultLogger( level, entry => {
20
+ // Already reported by Output activity logs and traces.
21
+ if ( OMITTED_MESSAGES.includes( entry.message ) ) {
22
+ return;
23
+ }
24
+ log.log( temporalLevelToInternalLevelMap[entry.level], entry.message, entry.meta );
25
+ } ),
26
+ telemetryOptions: {
27
+ logging: {
28
+ forward: {},
29
+ filter: makeTelemetryFilterString( { core: level, other: level } )
30
+ }
31
+ }
32
+ } );
33
+ };
@@ -0,0 +1,90 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ defaultLoggerMock,
5
+ logMock,
6
+ makeTelemetryFilterStringMock,
7
+ runtimeInstallMock
8
+ } = vi.hoisted( () => ( {
9
+ defaultLoggerMock: vi.fn( function ( level, callback ) {
10
+ this.level = level;
11
+ this.callback = callback;
12
+ } ),
13
+ logMock: { log: vi.fn() },
14
+ makeTelemetryFilterStringMock: vi.fn().mockReturnValue( 'temporal-filter' ),
15
+ runtimeInstallMock: vi.fn()
16
+ } ) );
17
+
18
+ vi.mock( '@temporalio/worker', () => ( {
19
+ DefaultLogger: defaultLoggerMock,
20
+ makeTelemetryFilterString: makeTelemetryFilterStringMock,
21
+ Runtime: { install: runtimeInstallMock }
22
+ } ) );
23
+ vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
24
+
25
+ import { setupTemporalLogger } from './temporal_logger.js';
26
+
27
+ describe( 'setupTemporalLogger', () => {
28
+ beforeEach( () => {
29
+ vi.clearAllMocks();
30
+ vi.stubEnv( 'OUTPUT_TEMPORAL_LOG_LEVEL', undefined );
31
+ } );
32
+
33
+ afterEach( () => {
34
+ vi.unstubAllEnvs();
35
+ } );
36
+
37
+ it( 'forwards Temporal SDK and Core logs through the application logger', () => {
38
+ setupTemporalLogger();
39
+
40
+ expect( makeTelemetryFilterStringMock ).toHaveBeenCalledWith( { core: 'INFO', other: 'INFO' } );
41
+ expect( defaultLoggerMock ).toHaveBeenCalledWith( 'INFO', expect.any( Function ) );
42
+ expect( runtimeInstallMock ).toHaveBeenCalledWith( {
43
+ logger: expect.any( defaultLoggerMock ),
44
+ telemetryOptions: {
45
+ logging: {
46
+ forward: {},
47
+ filter: 'temporal-filter'
48
+ }
49
+ }
50
+ } );
51
+
52
+ const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
53
+ entryCallback( { level: 'WARN', message: 'Worker warning', meta: { taskQueue: 'default' } } );
54
+
55
+ expect( logMock.log ).toHaveBeenCalledWith( 'warn', 'Worker warning', { taskQueue: 'default' } );
56
+ } );
57
+
58
+ it.each( [
59
+ [ 'TRACE', 'debug' ],
60
+ [ 'DEBUG', 'debug' ],
61
+ [ 'INFO', 'info' ],
62
+ [ 'WARN', 'warn' ],
63
+ [ 'ERROR', 'error' ]
64
+ ] )( 'maps Temporal %s logs to the %s application level', ( temporalLevel, internalLevel ) => {
65
+ setupTemporalLogger();
66
+ const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
67
+
68
+ entryCallback( { level: temporalLevel, message: `${temporalLevel} event`, meta: { source: 'test' } } );
69
+
70
+ expect( logMock.log ).toHaveBeenCalledWith( internalLevel, `${temporalLevel} event`, { source: 'test' } );
71
+ } );
72
+
73
+ it( 'applies a case-insensitive configured log level to SDK and Core logs', () => {
74
+ vi.stubEnv( 'OUTPUT_TEMPORAL_LOG_LEVEL', 'debug' );
75
+
76
+ setupTemporalLogger();
77
+
78
+ expect( defaultLoggerMock ).toHaveBeenCalledWith( 'DEBUG', expect.any( Function ) );
79
+ expect( makeTelemetryFilterStringMock ).toHaveBeenCalledWith( { core: 'DEBUG', other: 'DEBUG' } );
80
+ } );
81
+
82
+ it.each( [ 'Activity failed', 'Workflow failed' ] )( 'omits redundant "%s" events', message => {
83
+ setupTemporalLogger();
84
+ const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
85
+
86
+ entryCallback( { level: 'WARN', message, meta: { error: new Error( 'failed' ) } } );
87
+
88
+ expect( logMock.log ).not.toHaveBeenCalled();
89
+ } );
90
+ } );
@@ -7,3 +7,9 @@ export const ComponentFile = {
7
7
  STEPS: 'steps',
8
8
  WORKFLOW: 'workflow'
9
9
  };
10
+
11
+ export const ComponentFactory = {
12
+ EVALUATOR: 'evaluator',
13
+ STEP: 'step',
14
+ WORKFLOW: 'workflow'
15
+ };