@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.
- package/package.json +13 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +7 -3
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +23 -14
- package/src/helpers/errors.spec.js +31 -75
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/object.js +48 -35
- package/src/helpers/object.spec.js +123 -75
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +76 -78
- package/src/hooks/index.js +57 -49
- package/src/hooks/index.spec.js +104 -105
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +64 -85
- package/src/interface/workflow.spec.js +220 -226
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/sdk/helpers/objects.d.ts +24 -19
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +24 -11
- package/src/worker/index.spec.js +36 -16
- package/src/worker/interceptors/activity.js +34 -21
- package/src/worker/interceptors/activity.spec.js +77 -172
- package/src/worker/interceptors/workflow.js +58 -16
- package/src/worker/interceptors/workflow.spec.js +183 -56
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +36 -5
- package/src/worker/log_hooks.js +22 -19
- package/src/worker/log_hooks.spec.js +14 -6
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
249
|
-
const [ contents, filename ] = writeFileInTempDirMock.mock.calls[
|
|
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
|
} );
|
package/src/worker/log_hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
shouldLogActivity( activityInfo ) &&
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
shouldLogWorkflow( workflowDetails ) &&
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
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', () => ( {
|
|
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:
|
|
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 =
|
|
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', () => {
|
package/src/worker/sinks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BusEventType, ComponentType } from '#consts';
|
|
2
2
|
import * as Tracing from '#tracing';
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
if ( traceInfo ) {
|
|
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
|
-
|
|
41
|
-
if ( traceInfo ) {
|
|
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
|
-
|
|
52
|
-
if ( traceInfo ) {
|
|
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
|
},
|
package/src/worker/sinks.spec.js
CHANGED
|
@@ -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
|
|
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', () => ( {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
149
|
+
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
|
|
150
150
|
expect( addEventErrorMock ).not.toHaveBeenCalled();
|
|
151
151
|
} );
|
|
152
152
|
|
package/src/worker/telemetry.js
CHANGED
|
@@ -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
|
|
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
|
+
} );
|