@output.ai/core 0.5.11 → 0.5.13
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 +5 -1
- package/src/consts.js +19 -4
- package/src/event_hub/index.d.ts +7 -0
- package/src/event_hub/index.js +6 -0
- package/src/hooks/index.d.ts +9 -1
- package/src/hooks/index.js +16 -5
- package/src/interface/webhook.js +2 -2
- package/src/interface/webhook.spec.js +1 -1
- package/src/tracing/processors/s3/index.js +1 -1
- package/src/tracing/processors/s3/index.spec.js +42 -19
- package/src/tracing/tools/build_trace_tree.spec.js +7 -7
- package/src/worker/index.js +3 -1
- package/src/worker/index.spec.js +6 -1
- package/src/worker/interceptors/activity.js +13 -25
- package/src/worker/interceptors/activity.spec.js +15 -0
- package/src/worker/interceptors/workflow.js +4 -4
- package/src/worker/interceptors/workflow.spec.js +167 -0
- package/src/worker/log_hooks.js +95 -0
- package/src/worker/log_hooks.spec.js +217 -0
- package/src/worker/sinks.js +48 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@output.ai/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"./context": {
|
|
24
24
|
"types": "./src/context/index.d.ts",
|
|
25
25
|
"import": "./src/context/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./event_hub": {
|
|
28
|
+
"types": "./src/event_hub/index.d.ts",
|
|
29
|
+
"import": "./src/event_hub/index.js"
|
|
26
30
|
}
|
|
27
31
|
},
|
|
28
32
|
"files": [
|
package/src/consts.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
|
|
2
1
|
export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
|
|
2
|
+
export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
|
|
3
|
+
export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
|
|
3
4
|
export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
4
5
|
export const SHARED_STEP_PREFIX = '$shared';
|
|
5
|
-
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
6
|
-
export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
|
|
7
6
|
export const WORKFLOW_CATALOG = '$catalog';
|
|
7
|
+
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
8
|
+
|
|
8
9
|
export const ComponentType = {
|
|
9
10
|
EVALUATOR: 'evaluator',
|
|
10
11
|
INTERNAL_STEP: 'internal_step',
|
|
11
|
-
STEP: 'step'
|
|
12
|
+
STEP: 'step',
|
|
13
|
+
WORKFLOW: 'workflow'
|
|
12
14
|
};
|
|
15
|
+
|
|
13
16
|
export const LifecycleEvent = {
|
|
14
17
|
START: 'start',
|
|
15
18
|
END: 'end',
|
|
16
19
|
ERROR: 'error'
|
|
17
20
|
};
|
|
21
|
+
|
|
22
|
+
export const BusEventType = {
|
|
23
|
+
WORKFLOW_START: 'workflow:start',
|
|
24
|
+
WORKFLOW_END: 'workflow:end',
|
|
25
|
+
WORKFLOW_ERROR: 'workflow:error',
|
|
26
|
+
|
|
27
|
+
ACTIVITY_START: 'activity:start',
|
|
28
|
+
ACTIVITY_END: 'activity:end',
|
|
29
|
+
ACTIVITY_ERROR: 'activity:error',
|
|
30
|
+
|
|
31
|
+
RUNTIME_ERROR: 'runtime_error'
|
|
32
|
+
};
|
package/src/hooks/index.d.ts
CHANGED
|
@@ -13,8 +13,16 @@ export interface ErrorHookPayload {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Register a handler invoked on workflow, activity or runtime errors.
|
|
16
|
+
* Register a handler to be invoked on workflow, activity or runtime errors.
|
|
17
17
|
*
|
|
18
18
|
* @param handler - Function called with the error payload.
|
|
19
19
|
*/
|
|
20
20
|
export declare function onError( handler: ( payload: ErrorHookPayload ) => void ): void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a handler to be invoked when a given event happens
|
|
24
|
+
*
|
|
25
|
+
* @param eventName - The name of the event to subscribe
|
|
26
|
+
* @param handler - Function called with the event payload
|
|
27
|
+
*/
|
|
28
|
+
export declare function on( eventName: string, handler: ( payload: object ) => void ): void;
|
package/src/hooks/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messageBus } from '#bus';
|
|
2
|
+
import { BusEventType } from '#consts';
|
|
2
3
|
import { createChildLogger } from '#logger';
|
|
3
4
|
|
|
4
5
|
const log = createChildLogger( 'Hooks' );
|
|
@@ -12,10 +13,20 @@ export const onError = handler => {
|
|
|
12
13
|
}
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
messageBus.on(
|
|
16
|
-
invokeHandler( { source: 'activity',
|
|
17
|
-
messageBus.on(
|
|
18
|
-
invokeHandler( { source: 'workflow', workflowName, error } ) );
|
|
19
|
-
messageBus.on(
|
|
16
|
+
messageBus.on( BusEventType.ACTIVITY_ERROR, async ( { name, workflowName, error } ) =>
|
|
17
|
+
invokeHandler( { source: 'activity', activityName: name, workflowName, error } ) );
|
|
18
|
+
messageBus.on( BusEventType.WORKFLOW_ERROR, async ( { name, error } ) =>
|
|
19
|
+
invokeHandler( { source: 'workflow', workflowName: name, error } ) );
|
|
20
|
+
messageBus.on( BusEventType.RUNTIME_ERROR, async ( { error } ) =>
|
|
20
21
|
invokeHandler( { source: 'runtime', error } ) );
|
|
21
22
|
};
|
|
23
|
+
|
|
24
|
+
export const on = ( eventName, handler ) => {
|
|
25
|
+
messageBus.on( `external:${eventName}`, async payload => {
|
|
26
|
+
try {
|
|
27
|
+
await handler( payload );
|
|
28
|
+
} catch ( error ) {
|
|
29
|
+
log.error( `on(${eventName}) hook error`, { error } );
|
|
30
|
+
}
|
|
31
|
+
} );
|
|
32
|
+
};
|
package/src/interface/webhook.js
CHANGED
|
@@ -51,11 +51,11 @@ export async function sendPostRequestAndAwaitWebhook( { url, payload = undefined
|
|
|
51
51
|
const resumeSignal = defineSignal( 'resume' );
|
|
52
52
|
|
|
53
53
|
const traceId = `${workflowId}-${url}-${uuid4()}`;
|
|
54
|
-
sinks.trace.
|
|
54
|
+
sinks.trace.start( { id: traceId, name: 'resume', kind: 'webhook' } );
|
|
55
55
|
|
|
56
56
|
setHandler( resumeSignal, webhookPayload => {
|
|
57
57
|
if ( !resumeTrigger.resolved ) {
|
|
58
|
-
sinks.trace.
|
|
58
|
+
sinks.trace.end( { id: traceId, details: webhookPayload } );
|
|
59
59
|
resumeTrigger.resolve( webhookPayload );
|
|
60
60
|
}
|
|
61
61
|
} );
|
|
@@ -21,7 +21,7 @@ const setHandlerMock = ( signal, fn ) => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const workflowInfoMock = vi.fn( () => ( { workflowId: 'wf-123' } ) );
|
|
24
|
-
const sinks = { trace: {
|
|
24
|
+
const sinks = { trace: { start: vi.fn(), end: vi.fn() } };
|
|
25
25
|
const proxySinksMock = vi.fn( async () => sinks );
|
|
26
26
|
|
|
27
27
|
class TestTrigger {
|
|
@@ -78,7 +78,7 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
78
78
|
|
|
79
79
|
await addEntry( entry, cacheKey );
|
|
80
80
|
|
|
81
|
-
const isRootWorkflowEnd =
|
|
81
|
+
const isRootWorkflowEnd = entry.id === workflowId && entry.phase !== 'start';
|
|
82
82
|
if ( !isRootWorkflowEnd ) {
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
@@ -50,36 +50,31 @@ describe( 'tracing/processors/s3', () => {
|
|
|
50
50
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
51
51
|
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
52
52
|
|
|
53
|
-
// multi().exec() just needs to resolve for addEntry calls
|
|
54
53
|
redisMulti.exec.mockResolvedValue( [] );
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', phase: 'start', details: {}, timestamp: startTime };
|
|
56
|
+
const activityStart = { id: 'act-1', name: 'DoSomething', kind: 'step', parentId: 'id1', phase: 'start', details: {}, timestamp: startTime + 1 };
|
|
57
|
+
const workflowEnd = { id: 'id1', phase: 'end', details: { ok: true }, timestamp: startTime + 2 };
|
|
57
58
|
zRangeMock.mockResolvedValue( [
|
|
58
|
-
JSON.stringify(
|
|
59
|
-
JSON.stringify(
|
|
60
|
-
JSON.stringify(
|
|
59
|
+
JSON.stringify( workflowStart ),
|
|
60
|
+
JSON.stringify( activityStart ),
|
|
61
|
+
JSON.stringify( workflowEnd )
|
|
61
62
|
] );
|
|
62
63
|
|
|
63
|
-
await exec( { ...ctx, entry:
|
|
64
|
-
await exec( { ...ctx, entry:
|
|
65
|
-
// Root end:
|
|
66
|
-
const endPromise = exec( { ...ctx, entry:
|
|
64
|
+
await exec( { ...ctx, entry: workflowStart } );
|
|
65
|
+
await exec( { ...ctx, entry: activityStart } );
|
|
66
|
+
// Root end: id matches workflowId and not start — triggers the 10s delay before upload
|
|
67
|
+
const endPromise = exec( { ...ctx, entry: workflowEnd } );
|
|
67
68
|
await vi.advanceTimersByTimeAsync( 10_000 );
|
|
68
69
|
await endPromise;
|
|
69
70
|
|
|
70
|
-
// Accumulation happened 3 times
|
|
71
71
|
expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 3 );
|
|
72
|
-
|
|
73
|
-
// Tree is only built once at the end (not on every event)
|
|
74
72
|
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
75
73
|
expect( zRangeMock ).toHaveBeenCalledTimes( 1 );
|
|
76
|
-
|
|
77
|
-
// Only last call triggers upload
|
|
78
74
|
expect( uploadMock ).toHaveBeenCalledTimes( 1 );
|
|
79
75
|
const { key, content } = uploadMock.mock.calls[0][0];
|
|
80
76
|
expect( key ).toMatch( /^WF\/2020\/01\/02\// );
|
|
81
77
|
expect( JSON.parse( content.trim() ).count ).toBe( 3 );
|
|
82
|
-
|
|
83
78
|
expect( delMock ).toHaveBeenCalledTimes( 1 );
|
|
84
79
|
expect( delMock ).toHaveBeenCalledWith( 'traces/WF/id1' );
|
|
85
80
|
} );
|
|
@@ -101,24 +96,52 @@ describe( 'tracing/processors/s3', () => {
|
|
|
101
96
|
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
102
97
|
|
|
103
98
|
redisMulti.exec.mockResolvedValue( [] );
|
|
104
|
-
|
|
99
|
+
const workflowStart = {
|
|
100
|
+
kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, phase: 'start', details: {}, timestamp: startTime
|
|
101
|
+
};
|
|
102
|
+
zRangeMock.mockResolvedValue( [ JSON.stringify( workflowStart ) ] );
|
|
105
103
|
|
|
106
|
-
await exec( { ...ctx, entry:
|
|
104
|
+
await exec( { ...ctx, entry: workflowStart } );
|
|
107
105
|
|
|
108
106
|
expect( redisMulti.expire ).toHaveBeenCalledTimes( 1 );
|
|
109
107
|
expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/WF/id1', 3600 );
|
|
110
108
|
} );
|
|
111
109
|
|
|
110
|
+
it( 'exec(): does not treat a non-root end (e.g. step without parentId) as root workflow end — regression for wrong root detection', async () => {
|
|
111
|
+
const { exec } = await import( './index.js' );
|
|
112
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
113
|
+
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
114
|
+
|
|
115
|
+
redisMulti.exec.mockResolvedValue( [] );
|
|
116
|
+
const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', phase: 'start', details: {}, timestamp: startTime };
|
|
117
|
+
const stepEndNoParent = { id: 'step-1', phase: 'end', details: { done: true }, timestamp: startTime + 1 };
|
|
118
|
+
zRangeMock.mockResolvedValue( [
|
|
119
|
+
JSON.stringify( workflowStart ),
|
|
120
|
+
JSON.stringify( stepEndNoParent )
|
|
121
|
+
] );
|
|
122
|
+
|
|
123
|
+
await exec( { ...ctx, entry: workflowStart } );
|
|
124
|
+
await exec( { ...ctx, entry: stepEndNoParent } );
|
|
125
|
+
|
|
126
|
+
expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 2 );
|
|
127
|
+
expect( buildTraceTreeMock ).not.toHaveBeenCalled();
|
|
128
|
+
expect( uploadMock ).not.toHaveBeenCalled();
|
|
129
|
+
expect( delMock ).not.toHaveBeenCalled();
|
|
130
|
+
} );
|
|
131
|
+
|
|
112
132
|
it( 'exec(): when buildTraceTree returns null (incomplete tree), does not upload or bust cache', async () => {
|
|
113
133
|
const { exec } = await import( './index.js' );
|
|
114
134
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
115
135
|
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
116
136
|
|
|
117
137
|
redisMulti.exec.mockResolvedValue( [] );
|
|
118
|
-
|
|
138
|
+
const workflowEnd = {
|
|
139
|
+
kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, phase: 'end', details: {}, timestamp: startTime
|
|
140
|
+
};
|
|
141
|
+
zRangeMock.mockResolvedValue( [ JSON.stringify( workflowEnd ) ] );
|
|
119
142
|
buildTraceTreeMock.mockReturnValueOnce( null );
|
|
120
143
|
|
|
121
|
-
const endPromise = exec( { ...ctx, entry:
|
|
144
|
+
const endPromise = exec( { ...ctx, entry: workflowEnd } );
|
|
122
145
|
await vi.advanceTimersByTimeAsync( 10_000 );
|
|
123
146
|
await endPromise;
|
|
124
147
|
|
|
@@ -29,7 +29,7 @@ this can indicate it timed out or was interrupted.>>' );
|
|
|
29
29
|
const entries = [
|
|
30
30
|
{ kind: 'wf', id: 'r', parentId: undefined, phase: 'start', name: 'root', details: {}, timestamp: 100 },
|
|
31
31
|
{ kind: 'step', id: 's', parentId: 'r', phase: 'start', name: 'step', details: {}, timestamp: 200 },
|
|
32
|
-
{
|
|
32
|
+
{ id: 's', phase: 'error', details: { message: 'failed' }, timestamp: 300 }
|
|
33
33
|
];
|
|
34
34
|
const result = buildTraceTree( entries );
|
|
35
35
|
expect( result ).not.toBeNull();
|
|
@@ -44,7 +44,7 @@ this can indicate it timed out or was interrupted.>>' );
|
|
|
44
44
|
{ kind: 'workflow', phase: 'start', name: 'wf', id: 'wf', parentId: undefined, details: { a: 1 }, timestamp: 1000 },
|
|
45
45
|
// evaluator start/stop
|
|
46
46
|
{ kind: 'evaluator', phase: 'start', name: 'eval', id: 'eval', parentId: 'wf', details: { z: 0 }, timestamp: 1500 },
|
|
47
|
-
{
|
|
47
|
+
{ id: 'eval', phase: 'end', details: { z: 1 }, timestamp: 1600 },
|
|
48
48
|
// step1 start
|
|
49
49
|
{ kind: 'step', phase: 'start', name: 'step-1', id: 's1', parentId: 'wf', details: { x: 1 }, timestamp: 2000 },
|
|
50
50
|
// IO under step1
|
|
@@ -53,15 +53,15 @@ this can indicate it timed out or was interrupted.>>' );
|
|
|
53
53
|
{ kind: 'step', phase: 'start', name: 'step-2', id: 's2', parentId: 'wf', details: { x: 2 }, timestamp: 2400 },
|
|
54
54
|
// IO under step2
|
|
55
55
|
{ kind: 'IO', phase: 'start', name: 'test-2', id: 'io2', parentId: 's2', details: { y: 3 }, timestamp: 2500 },
|
|
56
|
-
{
|
|
56
|
+
{ id: 'io2', phase: 'end', details: { y: 4 }, timestamp: 2600 },
|
|
57
57
|
// IO under step1 ends
|
|
58
|
-
{
|
|
58
|
+
{ id: 'io1', phase: 'end', details: { y: 5 }, timestamp: 2700 },
|
|
59
59
|
// step1 end
|
|
60
|
-
{
|
|
60
|
+
{ id: 's1', phase: 'end', details: { done: true }, timestamp: 2800 },
|
|
61
61
|
// step2 end
|
|
62
|
-
{
|
|
62
|
+
{ id: 's2', phase: 'end', details: { done: true }, timestamp: 2900 },
|
|
63
63
|
// workflow end
|
|
64
|
-
{
|
|
64
|
+
{ id: 'wf', phase: 'end', details: { ok: true }, timestamp: 3000 }
|
|
65
65
|
];
|
|
66
66
|
|
|
67
67
|
const result = buildTraceTree( entries );
|
package/src/worker/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { createChildLogger } from '#logger';
|
|
|
10
10
|
import { registerShutdown } from './shutdown.js';
|
|
11
11
|
import { startCatalog } from './start_catalog.js';
|
|
12
12
|
import { messageBus } from '#bus';
|
|
13
|
+
import './log_hooks.js';
|
|
14
|
+
import { BusEventType } from '#consts';
|
|
13
15
|
|
|
14
16
|
const log = createChildLogger( 'Worker' );
|
|
15
17
|
|
|
@@ -81,7 +83,7 @@ const callerDir = process.argv[2];
|
|
|
81
83
|
process.exit( 0 );
|
|
82
84
|
} )().catch( error => {
|
|
83
85
|
log.error( 'Fatal error', { message: error.message, stack: error.stack } );
|
|
84
|
-
messageBus.emit(
|
|
86
|
+
messageBus.emit( BusEventType.RUNTIME_ERROR, { error } );
|
|
85
87
|
log.info( `Exiting in ${configs.processFailureShutdownDelay}ms` );
|
|
86
88
|
setTimeout( () => process.exit( 1 ), configs.processFailureShutdownDelay );
|
|
87
89
|
} );
|
package/src/worker/index.spec.js
CHANGED
|
@@ -3,7 +3,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
3
3
|
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
4
4
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
5
5
|
|
|
6
|
-
vi.mock( '#consts',
|
|
6
|
+
vi.mock( '#consts', async importOriginal => {
|
|
7
|
+
const actual = await importOriginal();
|
|
8
|
+
return { ...actual };
|
|
9
|
+
} );
|
|
7
10
|
|
|
8
11
|
vi.mock( '#tracing', () => ( { init: vi.fn().mockResolvedValue( undefined ) } ) );
|
|
9
12
|
|
|
@@ -52,6 +55,8 @@ vi.mock( './start_catalog.js', () => ( { startCatalog: startCatalogMock } ) );
|
|
|
52
55
|
const registerShutdownMock = vi.fn();
|
|
53
56
|
vi.mock( './shutdown.js', () => ( { registerShutdown: registerShutdownMock } ) );
|
|
54
57
|
|
|
58
|
+
vi.mock( './log_hooks.js', () => ( {} ) );
|
|
59
|
+
|
|
55
60
|
const runState = { resolve: null };
|
|
56
61
|
const runPromise = new Promise( r => {
|
|
57
62
|
runState.resolve = r;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { Context } from '@temporalio/activity';
|
|
2
2
|
import { Storage } from '#async_storage';
|
|
3
|
-
import
|
|
3
|
+
import * as Tracing from '#tracing';
|
|
4
4
|
import { headersToObject } from '../sandboxed_utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
6
|
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
|
|
7
|
-
import { createChildLogger } from '#logger';
|
|
8
7
|
import { messageBus } from '#bus';
|
|
9
8
|
|
|
10
|
-
const log = createChildLogger( 'Activity' );
|
|
11
9
|
/*
|
|
12
10
|
This interceptor wraps every activity execution with cross-cutting concerns:
|
|
13
11
|
|
|
@@ -31,16 +29,14 @@ export class ActivityExecutionInterceptor {
|
|
|
31
29
|
};
|
|
32
30
|
|
|
33
31
|
async execute( input, next ) {
|
|
34
|
-
const { workflowExecution: { workflowId }, activityId, activityType, workflowType: workflowName } = Context.current().info;
|
|
35
|
-
const { executionContext } = headersToObject( input.headers );
|
|
36
|
-
const { type: kind } = this.activities?.[activityType]?.[METADATA_ACCESS_SYMBOL];
|
|
37
|
-
|
|
38
32
|
const startDate = Date.now();
|
|
39
|
-
const
|
|
40
|
-
const
|
|
33
|
+
const { workflowExecution: { workflowId }, activityId: id, activityType: name, workflowType: workflowName } = Context.current().info;
|
|
34
|
+
const { executionContext } = headersToObject( input.headers );
|
|
35
|
+
const { type: kind } = this.activities?.[name]?.[METADATA_ACCESS_SYMBOL];
|
|
36
|
+
const workflowFilename = this.workflowsMap.get( workflowName ).path;
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
addEventStart( { details: input.args[0],
|
|
38
|
+
messageBus.emit( BusEventType.ACTIVITY_START, { id, name, kind, workflowId, workflowName } );
|
|
39
|
+
Tracing.addEventStart( { id, name, kind, parentId: workflowId, details: input.args[0], executionContext } );
|
|
44
40
|
|
|
45
41
|
const intervals = { heartbeat: null };
|
|
46
42
|
try {
|
|
@@ -48,25 +44,17 @@ export class ActivityExecutionInterceptor {
|
|
|
48
44
|
intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
|
|
49
45
|
|
|
50
46
|
// Wraps the execution with accessible metadata for the activity
|
|
51
|
-
const output = await Storage.runWithContext( async _ => next( input ), {
|
|
52
|
-
parentId: activityId,
|
|
53
|
-
executionContext,
|
|
54
|
-
workflowFilename: this.workflowsMap.get( workflowName ).path
|
|
55
|
-
} );
|
|
47
|
+
const output = await Storage.runWithContext( async _ => next( input ), { parentId: id, executionContext, workflowFilename } );
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
addEventEnd( { details: output,
|
|
49
|
+
messageBus.emit( BusEventType.ACTIVITY_END, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate } );
|
|
50
|
+
Tracing.addEventEnd( { id, details: output, executionContext } );
|
|
59
51
|
return output;
|
|
60
52
|
|
|
61
53
|
} catch ( error ) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
} );
|
|
65
|
-
addEventError( { details: error, ...traceArguments } );
|
|
54
|
+
messageBus.emit( BusEventType.ACTIVITY_ERROR, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate, error } );
|
|
55
|
+
Tracing.addEventError( { id, details: error, executionContext } );
|
|
66
56
|
|
|
67
|
-
messageBus.emit( 'activity:error', { error, workflowName, activityName: activityType } );
|
|
68
57
|
throw error;
|
|
69
|
-
|
|
70
58
|
} finally {
|
|
71
59
|
clearInterval( intervals.heartbeat );
|
|
72
60
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { BusEventType } from '#consts';
|
|
2
3
|
|
|
3
4
|
const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
|
|
4
5
|
|
|
@@ -39,6 +40,9 @@ vi.mock( '../sandboxed_utils.js', () => ( {
|
|
|
39
40
|
headersToObject: () => ( { executionContext: { workflowId: 'wf-1' } } )
|
|
40
41
|
} ) );
|
|
41
42
|
|
|
43
|
+
const messageBusEmitMock = vi.fn();
|
|
44
|
+
vi.mock( '#bus', () => ( { messageBus: { emit: messageBusEmitMock } } ) );
|
|
45
|
+
|
|
42
46
|
vi.mock( '#consts', async importOriginal => {
|
|
43
47
|
const actual = await importOriginal();
|
|
44
48
|
return {
|
|
@@ -93,6 +97,12 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
93
97
|
const output = await promise;
|
|
94
98
|
|
|
95
99
|
expect( output ).toEqual( { result: 'ok' } );
|
|
100
|
+
expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.objectContaining( {
|
|
101
|
+
id: 'act-1', name: 'myWorkflow#myStep', kind: 'step', workflowId: 'wf-1', workflowName: 'myWorkflow'
|
|
102
|
+
} ) );
|
|
103
|
+
expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.objectContaining( {
|
|
104
|
+
id: 'act-1', name: 'myWorkflow#myStep', kind: 'step', workflowId: 'wf-1', workflowName: 'myWorkflow', duration: expect.any( Number )
|
|
105
|
+
} ) );
|
|
96
106
|
expect( addEventStartMock ).toHaveBeenCalledOnce();
|
|
97
107
|
expect( addEventEndMock ).toHaveBeenCalledOnce();
|
|
98
108
|
expect( addEventErrorMock ).not.toHaveBeenCalled();
|
|
@@ -116,6 +126,11 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
116
126
|
vi.advanceTimersByTime( 0 );
|
|
117
127
|
|
|
118
128
|
await expect( promise ).rejects.toThrow( 'step failed' );
|
|
129
|
+
expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
|
|
130
|
+
expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.objectContaining( {
|
|
131
|
+
id: 'act-1', name: 'myWorkflow#myStep', kind: 'step', workflowId: 'wf-1', workflowName: 'myWorkflow',
|
|
132
|
+
duration: expect.any( Number ), error: expect.any( Error )
|
|
133
|
+
} ) );
|
|
119
134
|
expect( addEventStartMock ).toHaveBeenCalledOnce();
|
|
120
135
|
expect( addEventErrorMock ).toHaveBeenCalledOnce();
|
|
121
136
|
expect( addEventEndMock ).not.toHaveBeenCalled();
|
|
@@ -32,10 +32,10 @@ const sinks = proxySinks();
|
|
|
32
32
|
|
|
33
33
|
class WorkflowExecutionInterceptor {
|
|
34
34
|
async execute( input, next ) {
|
|
35
|
-
sinks.
|
|
35
|
+
sinks.workflow.start( input.args[0] );
|
|
36
36
|
try {
|
|
37
37
|
const output = await next( input );
|
|
38
|
-
sinks.
|
|
38
|
+
sinks.workflow.end( output );
|
|
39
39
|
return output;
|
|
40
40
|
} catch ( error ) {
|
|
41
41
|
/*
|
|
@@ -44,11 +44,11 @@ class WorkflowExecutionInterceptor {
|
|
|
44
44
|
* a new trace file will be generated
|
|
45
45
|
*/
|
|
46
46
|
if ( error instanceof ContinueAsNew ) {
|
|
47
|
-
sinks.
|
|
47
|
+
sinks.workflow.end( '<continued_as_new>' );
|
|
48
48
|
throw error;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
sinks.
|
|
51
|
+
sinks.workflow.error( error );
|
|
52
52
|
const failure = new ApplicationFailure( error.message, error.constructor.name, undefined, undefined, error );
|
|
53
53
|
|
|
54
54
|
/*
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
|
|
4
|
+
|
|
5
|
+
const workflowInfoMock = vi.fn();
|
|
6
|
+
const workflowStartMock = vi.fn();
|
|
7
|
+
const workflowEndMock = vi.fn();
|
|
8
|
+
const workflowErrorMock = vi.fn();
|
|
9
|
+
vi.mock( '@temporalio/workflow', () => ( {
|
|
10
|
+
workflowInfo: ( ...args ) => workflowInfoMock( ...args ),
|
|
11
|
+
proxySinks: () => ( {
|
|
12
|
+
workflow: { start: workflowStartMock, end: workflowEndMock, error: workflowErrorMock }
|
|
13
|
+
} ),
|
|
14
|
+
ApplicationFailure: class ApplicationFailure {
|
|
15
|
+
constructor( message, type, nonRetryable, cause, originalError ) {
|
|
16
|
+
this.message = message;
|
|
17
|
+
this.type = type;
|
|
18
|
+
this.nonRetryable = nonRetryable;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
this.originalError = originalError;
|
|
21
|
+
this.details = undefined;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
ContinueAsNew: class ContinueAsNew extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super( 'ContinueAsNew' );
|
|
27
|
+
this.name = 'ContinueAsNew';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} ) );
|
|
31
|
+
|
|
32
|
+
const memoToHeadersMock = vi.fn( memo => ( memo ? { ...memo, __asHeaders: true } : {} ) );
|
|
33
|
+
vi.mock( '../sandboxed_utils.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMock( ...args ) } ) );
|
|
34
|
+
|
|
35
|
+
const deepMergeMock = vi.fn( ( a, b ) => ( { ...( a || {} ), ...( b || {} ) } ) );
|
|
36
|
+
vi.mock( '#utils', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
|
|
37
|
+
|
|
38
|
+
vi.mock( '#consts', async importOriginal => {
|
|
39
|
+
const actual = await importOriginal();
|
|
40
|
+
return {
|
|
41
|
+
...actual, get METADATA_ACCESS_SYMBOL() {
|
|
42
|
+
return METADATA_ACCESS_SYMBOL;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
const stepOptionsDefault = {};
|
|
48
|
+
vi.mock( '../temp/__activity_options.js', () => ( { default: stepOptionsDefault } ) );
|
|
49
|
+
|
|
50
|
+
describe( 'workflow interceptors', () => {
|
|
51
|
+
beforeEach( () => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
workflowInfoMock.mockReturnValue( { workflowType: 'MyWorkflow', memo: { executionContext: { id: 'ctx-1' } } } );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
describe( 'HeadersInjectionInterceptor', () => {
|
|
57
|
+
it( 'assigns memo as headers via memoToHeaders and calls next', async () => {
|
|
58
|
+
const { interceptors } = await import( './workflow.js' );
|
|
59
|
+
const { outbound } = interceptors();
|
|
60
|
+
const interceptor = outbound[0];
|
|
61
|
+
const input = { headers: { existing: 'header' }, activityType: 'MyWorkflow#step1' };
|
|
62
|
+
const next = vi.fn().mockResolvedValue( 'result' );
|
|
63
|
+
|
|
64
|
+
memoToHeadersMock.mockReturnValue( { executionContext: { id: 'ctx-1' } } );
|
|
65
|
+
|
|
66
|
+
const out = await interceptor.scheduleActivity( input, next );
|
|
67
|
+
|
|
68
|
+
expect( memoToHeadersMock ).toHaveBeenCalledWith( { executionContext: { id: 'ctx-1' } } );
|
|
69
|
+
expect( input.headers ).toEqual( { existing: 'header', executionContext: { id: 'ctx-1' } } );
|
|
70
|
+
expect( next ).toHaveBeenCalledWith( input );
|
|
71
|
+
expect( out ).toBe( 'result' );
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
it( 'merges stepOptions with memo.activityOptions when stepOptions exist for activityType', async () => {
|
|
75
|
+
stepOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
|
|
76
|
+
workflowInfoMock.mockReturnValue( {
|
|
77
|
+
workflowType: 'MyWorkflow',
|
|
78
|
+
memo: { executionContext: {}, activityOptions: { heartbeatTimeout: 10 } }
|
|
79
|
+
} );
|
|
80
|
+
memoToHeadersMock.mockReturnValue( {} );
|
|
81
|
+
deepMergeMock.mockReturnValue( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );
|
|
82
|
+
|
|
83
|
+
const { interceptors } = await import( './workflow.js' );
|
|
84
|
+
const { outbound } = interceptors();
|
|
85
|
+
const interceptor = outbound[0];
|
|
86
|
+
const input = { headers: {}, activityType: 'MyWorkflow#step1' };
|
|
87
|
+
const next = vi.fn().mockResolvedValue( undefined );
|
|
88
|
+
|
|
89
|
+
await interceptor.scheduleActivity( input, next );
|
|
90
|
+
|
|
91
|
+
expect( deepMergeMock ).toHaveBeenCalledWith( { heartbeatTimeout: 10 }, { scheduleToCloseTimeout: 60 } );
|
|
92
|
+
expect( input.options ).toEqual( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );
|
|
93
|
+
delete stepOptionsDefault['MyWorkflow#step1'];
|
|
94
|
+
} );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
describe( 'WorkflowExecutionInterceptor', () => {
|
|
98
|
+
it( 'calls sinks.workflow.start, next, then sinks.workflow.end on success', async () => {
|
|
99
|
+
const { interceptors } = await import( './workflow.js' );
|
|
100
|
+
const { inbound } = interceptors();
|
|
101
|
+
const interceptor = inbound[0];
|
|
102
|
+
const input = { args: [ { input: 'data' } ] };
|
|
103
|
+
const next = vi.fn().mockResolvedValue( { output: 'ok' } );
|
|
104
|
+
|
|
105
|
+
const result = await interceptor.execute( input, next );
|
|
106
|
+
|
|
107
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( { input: 'data' } );
|
|
108
|
+
expect( next ).toHaveBeenCalledWith( input );
|
|
109
|
+
expect( workflowEndMock ).toHaveBeenCalledWith( { output: 'ok' } );
|
|
110
|
+
expect( result ).toEqual( { output: 'ok' } );
|
|
111
|
+
expect( workflowErrorMock ).not.toHaveBeenCalled();
|
|
112
|
+
} );
|
|
113
|
+
|
|
114
|
+
it( 'calls sinks.workflow.error and throws ApplicationFailure on error', async () => {
|
|
115
|
+
const { interceptors } = await import( './workflow.js' );
|
|
116
|
+
const { inbound } = interceptors();
|
|
117
|
+
const interceptor = inbound[0];
|
|
118
|
+
const input = { args: [ {} ] };
|
|
119
|
+
const err = new Error( 'workflow failed' );
|
|
120
|
+
const next = vi.fn().mockRejectedValue( err );
|
|
121
|
+
|
|
122
|
+
await expect( interceptor.execute( input, next ) ).rejects.toMatchObject( {
|
|
123
|
+
message: 'workflow failed',
|
|
124
|
+
type: 'Error',
|
|
125
|
+
originalError: err
|
|
126
|
+
} );
|
|
127
|
+
expect( workflowStartMock ).toHaveBeenCalled();
|
|
128
|
+
expect( workflowErrorMock ).toHaveBeenCalledWith( err );
|
|
129
|
+
expect( workflowEndMock ).not.toHaveBeenCalled();
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'sets failure.details from error metadata when present', async () => {
|
|
133
|
+
const { interceptors } = await import( './workflow.js' );
|
|
134
|
+
const { ApplicationFailure } = await import( '@temporalio/workflow' );
|
|
135
|
+
const { inbound } = interceptors();
|
|
136
|
+
const interceptor = inbound[0];
|
|
137
|
+
const meta = { code: 'CUSTOM' };
|
|
138
|
+
const err = new Error( 'custom' );
|
|
139
|
+
err[METADATA_ACCESS_SYMBOL] = meta;
|
|
140
|
+
const next = vi.fn().mockRejectedValue( err );
|
|
141
|
+
|
|
142
|
+
const error = await ( async () => {
|
|
143
|
+
try {
|
|
144
|
+
await interceptor.execute( { args: [ {} ] }, next );
|
|
145
|
+
return null;
|
|
146
|
+
} catch ( error ) {
|
|
147
|
+
return error;
|
|
148
|
+
}
|
|
149
|
+
} )();
|
|
150
|
+
expect( error ).toBeInstanceOf( ApplicationFailure );
|
|
151
|
+
expect( error.details ).toEqual( [ meta ] );
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'on ContinueAsNew calls sinks.trace.addWorkflowEventEnd and rethrows', async () => {
|
|
155
|
+
const { ContinueAsNew } = await import( '@temporalio/workflow' );
|
|
156
|
+
const { interceptors } = await import( './workflow.js' );
|
|
157
|
+
const { inbound } = interceptors();
|
|
158
|
+
const interceptor = inbound[0];
|
|
159
|
+
const continueErr = new ContinueAsNew();
|
|
160
|
+
const next = vi.fn().mockRejectedValue( continueErr );
|
|
161
|
+
|
|
162
|
+
await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toThrow( ContinueAsNew );
|
|
163
|
+
expect( workflowEndMock ).toHaveBeenCalledWith( '<continued_as_new>' );
|
|
164
|
+
expect( workflowErrorMock ).not.toHaveBeenCalled();
|
|
165
|
+
} );
|
|
166
|
+
} );
|
|
167
|
+
} );
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { messageBus } from '#bus';
|
|
2
|
+
import { createChildLogger } from '#logger';
|
|
3
|
+
import { BusEventType, ComponentType, LifecycleEvent, WORKFLOW_CATALOG } from '#consts';
|
|
4
|
+
|
|
5
|
+
const activityLog = createChildLogger( 'Activity' );
|
|
6
|
+
const workflowLog = createChildLogger( 'Workflow' );
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Intercepts internal bus events for activity and workflow lifecycle and log them
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
╔═════════════════╗
|
|
14
|
+
║ Activity events ║
|
|
15
|
+
╚═════════════════╝
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns true if activity event should be logged
|
|
20
|
+
*/
|
|
21
|
+
const shouldLogActivityEvent = ( { kind } ) => kind !== ComponentType.INTERNAL_STEP;
|
|
22
|
+
|
|
23
|
+
messageBus.on( BusEventType.ACTIVITY_START, ( { id, name, kind, workflowId, workflowName } ) =>
|
|
24
|
+
shouldLogActivityEvent( { kind } ) && activityLog.info( `Started ${name} ${kind}`, {
|
|
25
|
+
event: LifecycleEvent.START,
|
|
26
|
+
activityId: id,
|
|
27
|
+
activityName: name,
|
|
28
|
+
activityKind: kind,
|
|
29
|
+
workflowId,
|
|
30
|
+
workflowName
|
|
31
|
+
} )
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
messageBus.on( BusEventType.ACTIVITY_END, ( { id, name, kind, workflowId, workflowName, duration } ) =>
|
|
35
|
+
shouldLogActivityEvent( { kind } ) && activityLog.info( `Ended ${name} ${kind}`, {
|
|
36
|
+
event: LifecycleEvent.END,
|
|
37
|
+
activityId: id,
|
|
38
|
+
activityName: name,
|
|
39
|
+
activityKind: kind,
|
|
40
|
+
workflowId,
|
|
41
|
+
workflowName,
|
|
42
|
+
durationMs: duration
|
|
43
|
+
} )
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
messageBus.on( BusEventType.ACTIVITY_ERROR, ( { id, name, kind, workflowId, workflowName, duration, error } ) =>
|
|
47
|
+
shouldLogActivityEvent( { kind } ) && activityLog.error( `Error ${name} ${kind}: ${error.constructor.name}`, {
|
|
48
|
+
event: LifecycleEvent.ERROR,
|
|
49
|
+
activityId: id,
|
|
50
|
+
activityName: name,
|
|
51
|
+
activityKind: kind,
|
|
52
|
+
workflowId,
|
|
53
|
+
workflowName,
|
|
54
|
+
durationMs: duration,
|
|
55
|
+
error: error.message
|
|
56
|
+
} )
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
╔═════════════════╗
|
|
61
|
+
║ Workflow events ║
|
|
62
|
+
╚═════════════════╝
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if activity event should be logged
|
|
67
|
+
*/
|
|
68
|
+
const shouldLogWorkflowEvent = ( { name } ) => name !== WORKFLOW_CATALOG;
|
|
69
|
+
|
|
70
|
+
messageBus.on( BusEventType.WORKFLOW_START, ( { id, name } ) =>
|
|
71
|
+
shouldLogWorkflowEvent( { name } ) && workflowLog.info( `Started ${name} workflow`, {
|
|
72
|
+
event: LifecycleEvent.START,
|
|
73
|
+
workflowId: id,
|
|
74
|
+
workflowName: name
|
|
75
|
+
} )
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
messageBus.on( BusEventType.WORKFLOW_END, ( { id, name, duration } ) =>
|
|
79
|
+
shouldLogWorkflowEvent( { name } ) && workflowLog.info( `Ended ${name} workflow`, {
|
|
80
|
+
event: LifecycleEvent.END,
|
|
81
|
+
workflowId: id,
|
|
82
|
+
workflowName: name,
|
|
83
|
+
durationMs: duration
|
|
84
|
+
} )
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
messageBus.on( BusEventType.WORKFLOW_ERROR, ( { id, name, duration, error } ) =>
|
|
88
|
+
shouldLogWorkflowEvent( { name } ) && workflowLog.error( `Error ${name} workflow: ${error.constructor.name}`, {
|
|
89
|
+
event: LifecycleEvent.ERROR,
|
|
90
|
+
workflowId: id,
|
|
91
|
+
workflowName: name,
|
|
92
|
+
durationMs: duration,
|
|
93
|
+
error: error.message
|
|
94
|
+
} )
|
|
95
|
+
);
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
BusEventType,
|
|
4
|
+
ComponentType,
|
|
5
|
+
LifecycleEvent,
|
|
6
|
+
WORKFLOW_CATALOG
|
|
7
|
+
} from '#consts';
|
|
8
|
+
|
|
9
|
+
const activityLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
|
|
10
|
+
const workflowLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
|
|
11
|
+
const createChildLoggerMock = vi.hoisted( () =>
|
|
12
|
+
vi.fn( name => ( name === 'Activity' ? activityLogMock : workflowLogMock ) )
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const onHandlers = vi.hoisted( () => ( {} ) );
|
|
16
|
+
const messageBusMock = vi.hoisted( () => ( {
|
|
17
|
+
on: vi.fn( ( eventType, handler ) => {
|
|
18
|
+
onHandlers[eventType] = handler;
|
|
19
|
+
} )
|
|
20
|
+
} ) );
|
|
21
|
+
|
|
22
|
+
vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
|
|
23
|
+
vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
|
|
24
|
+
|
|
25
|
+
import './log_hooks.js';
|
|
26
|
+
|
|
27
|
+
describe( 'log_hooks', () => {
|
|
28
|
+
beforeEach( () => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
describe( 'activity events', () => {
|
|
33
|
+
const basePayload = {
|
|
34
|
+
id: 'act-1',
|
|
35
|
+
name: 'myWorkflow#myStep',
|
|
36
|
+
kind: 'step',
|
|
37
|
+
workflowId: 'wf-1',
|
|
38
|
+
workflowName: 'myWorkflow'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
it( 'ACTIVITY_START logs full message and second arg', () => {
|
|
42
|
+
onHandlers[BusEventType.ACTIVITY_START]( basePayload );
|
|
43
|
+
|
|
44
|
+
expect( activityLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
45
|
+
expect( activityLogMock.info ).toHaveBeenCalledWith(
|
|
46
|
+
'Started myWorkflow#myStep step',
|
|
47
|
+
{
|
|
48
|
+
event: LifecycleEvent.START,
|
|
49
|
+
activityId: 'act-1',
|
|
50
|
+
activityName: 'myWorkflow#myStep',
|
|
51
|
+
activityKind: 'step',
|
|
52
|
+
workflowId: 'wf-1',
|
|
53
|
+
workflowName: 'myWorkflow'
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'ACTIVITY_START does not log when kind is INTERNAL_STEP', () => {
|
|
59
|
+
onHandlers[BusEventType.ACTIVITY_START]( {
|
|
60
|
+
...basePayload,
|
|
61
|
+
kind: ComponentType.INTERNAL_STEP
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
expect( activityLogMock.info ).not.toHaveBeenCalled();
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'ACTIVITY_END logs full message and second arg', () => {
|
|
68
|
+
onHandlers[BusEventType.ACTIVITY_END]( { ...basePayload, duration: 42 } );
|
|
69
|
+
|
|
70
|
+
expect( activityLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
71
|
+
expect( activityLogMock.info ).toHaveBeenCalledWith(
|
|
72
|
+
'Ended myWorkflow#myStep step',
|
|
73
|
+
{
|
|
74
|
+
event: LifecycleEvent.END,
|
|
75
|
+
activityId: 'act-1',
|
|
76
|
+
activityName: 'myWorkflow#myStep',
|
|
77
|
+
activityKind: 'step',
|
|
78
|
+
workflowId: 'wf-1',
|
|
79
|
+
workflowName: 'myWorkflow',
|
|
80
|
+
durationMs: 42
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'ACTIVITY_END does not log when kind is INTERNAL_STEP', () => {
|
|
86
|
+
onHandlers[BusEventType.ACTIVITY_END]( {
|
|
87
|
+
...basePayload,
|
|
88
|
+
kind: ComponentType.INTERNAL_STEP,
|
|
89
|
+
duration: 10
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
expect( activityLogMock.info ).not.toHaveBeenCalled();
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'ACTIVITY_ERROR logs full message and second arg', () => {
|
|
96
|
+
const err = new Error( 'step failed' );
|
|
97
|
+
onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
98
|
+
...basePayload,
|
|
99
|
+
duration: 100,
|
|
100
|
+
error: err
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
expect( activityLogMock.error ).toHaveBeenCalledTimes( 1 );
|
|
104
|
+
expect( activityLogMock.error ).toHaveBeenCalledWith(
|
|
105
|
+
'Error myWorkflow#myStep step: Error',
|
|
106
|
+
{
|
|
107
|
+
event: LifecycleEvent.ERROR,
|
|
108
|
+
activityId: 'act-1',
|
|
109
|
+
activityName: 'myWorkflow#myStep',
|
|
110
|
+
activityKind: 'step',
|
|
111
|
+
workflowId: 'wf-1',
|
|
112
|
+
workflowName: 'myWorkflow',
|
|
113
|
+
durationMs: 100,
|
|
114
|
+
error: 'step failed'
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'ACTIVITY_ERROR does not log when kind is INTERNAL_STEP', () => {
|
|
120
|
+
onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
121
|
+
...basePayload,
|
|
122
|
+
kind: ComponentType.INTERNAL_STEP,
|
|
123
|
+
duration: 5,
|
|
124
|
+
error: new Error( 'x' )
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
expect( activityLogMock.error ).not.toHaveBeenCalled();
|
|
128
|
+
} );
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
describe( 'workflow events', () => {
|
|
132
|
+
const basePayload = { id: 'wf-1', name: 'myWorkflow' };
|
|
133
|
+
|
|
134
|
+
it( 'WORKFLOW_START logs full message and second arg', () => {
|
|
135
|
+
onHandlers[BusEventType.WORKFLOW_START]( basePayload );
|
|
136
|
+
|
|
137
|
+
expect( workflowLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
138
|
+
expect( workflowLogMock.info ).toHaveBeenCalledWith(
|
|
139
|
+
'Started myWorkflow workflow',
|
|
140
|
+
{
|
|
141
|
+
event: LifecycleEvent.START,
|
|
142
|
+
workflowId: 'wf-1',
|
|
143
|
+
workflowName: 'myWorkflow'
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
} );
|
|
147
|
+
|
|
148
|
+
it( 'WORKFLOW_START does not log when name is WORKFLOW_CATALOG', () => {
|
|
149
|
+
onHandlers[BusEventType.WORKFLOW_START]( {
|
|
150
|
+
id: 'cat-1',
|
|
151
|
+
name: WORKFLOW_CATALOG
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
expect( workflowLogMock.info ).not.toHaveBeenCalled();
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
it( 'WORKFLOW_END logs full message and second arg', () => {
|
|
158
|
+
onHandlers[BusEventType.WORKFLOW_END]( {
|
|
159
|
+
...basePayload,
|
|
160
|
+
duration: 200
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
expect( workflowLogMock.info ).toHaveBeenCalledTimes( 1 );
|
|
164
|
+
expect( workflowLogMock.info ).toHaveBeenCalledWith(
|
|
165
|
+
'Ended myWorkflow workflow',
|
|
166
|
+
{
|
|
167
|
+
event: LifecycleEvent.END,
|
|
168
|
+
workflowId: 'wf-1',
|
|
169
|
+
workflowName: 'myWorkflow',
|
|
170
|
+
durationMs: 200
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
it( 'WORKFLOW_END does not log when name is WORKFLOW_CATALOG', () => {
|
|
176
|
+
onHandlers[BusEventType.WORKFLOW_END]( {
|
|
177
|
+
id: 'cat-1',
|
|
178
|
+
name: WORKFLOW_CATALOG,
|
|
179
|
+
duration: 50
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
expect( workflowLogMock.info ).not.toHaveBeenCalled();
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
it( 'WORKFLOW_ERROR logs full message and second arg', () => {
|
|
186
|
+
const err = new TypeError( 'workflow boom' );
|
|
187
|
+
onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
188
|
+
...basePayload,
|
|
189
|
+
duration: 150,
|
|
190
|
+
error: err
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
expect( workflowLogMock.error ).toHaveBeenCalledTimes( 1 );
|
|
194
|
+
expect( workflowLogMock.error ).toHaveBeenCalledWith(
|
|
195
|
+
'Error myWorkflow workflow: TypeError',
|
|
196
|
+
{
|
|
197
|
+
event: LifecycleEvent.ERROR,
|
|
198
|
+
workflowId: 'wf-1',
|
|
199
|
+
workflowName: 'myWorkflow',
|
|
200
|
+
durationMs: 150,
|
|
201
|
+
error: 'workflow boom'
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'WORKFLOW_ERROR does not log when name is WORKFLOW_CATALOG', () => {
|
|
207
|
+
onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
208
|
+
id: 'cat-1',
|
|
209
|
+
name: WORKFLOW_CATALOG,
|
|
210
|
+
duration: 1,
|
|
211
|
+
error: new Error( 'x' )
|
|
212
|
+
} );
|
|
213
|
+
|
|
214
|
+
expect( workflowLogMock.error ).not.toHaveBeenCalled();
|
|
215
|
+
} );
|
|
216
|
+
} );
|
|
217
|
+
} );
|
package/src/worker/sinks.js
CHANGED
|
@@ -1,109 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { createChildLogger } from '#logger';
|
|
1
|
+
import { BusEventType, ComponentType } from '#consts';
|
|
2
|
+
import * as Tracing from '#tracing';
|
|
4
3
|
import { messageBus } from '#bus';
|
|
5
4
|
|
|
6
|
-
const log = createChildLogger( 'Workflow' );
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Adds a workflow trace event
|
|
10
|
-
*
|
|
11
|
-
* @param {function} method - Trace function to call
|
|
12
|
-
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
13
|
-
* @param {object} details - The details to attach to the event
|
|
14
|
-
*/
|
|
15
|
-
const addWorkflowEvent = ( method, workflowInfo, details ) => {
|
|
16
|
-
const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
|
|
17
|
-
if ( name === WORKFLOW_CATALOG ) {
|
|
18
|
-
return;
|
|
19
|
-
} // ignore internal catalog events
|
|
20
|
-
method( { id, kind: 'workflow', name, details, parentId, executionContext } );
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Logs the internal workflow event
|
|
25
|
-
*
|
|
26
|
-
* @param {LifecycleEvent} event
|
|
27
|
-
* @param {Object} workflowInfo
|
|
28
|
-
* @returns {void}
|
|
29
|
-
*/
|
|
30
|
-
const logWorkflowEvent = ( event, workflowInfo, error ) => {
|
|
31
|
-
const { workflowId, workflowType: workflowName, startTime } = workflowInfo;
|
|
32
|
-
// exclude internal catalog
|
|
33
|
-
if ( workflowName === WORKFLOW_CATALOG ) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if ( event === LifecycleEvent.START ) {
|
|
38
|
-
log.info( `Started ${workflowName} workflow`, { event, workflowName, workflowId } );
|
|
39
|
-
} else if ( event === LifecycleEvent.END ) {
|
|
40
|
-
log.info( `Ended ${workflowName} workflow`, { event, workflowName, workflowId, durationMs: Date.now() - startTime.getTime() } );
|
|
41
|
-
} else if ( event === LifecycleEvent.ERROR ) {
|
|
42
|
-
log.error( `Error ${workflowName} workflow: ${error.message}`, {
|
|
43
|
-
event,
|
|
44
|
-
workflowName,
|
|
45
|
-
workflowId,
|
|
46
|
-
durationMs: Date.now() - startTime.getTime(),
|
|
47
|
-
error: error.message
|
|
48
|
-
} );
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Start a trace event with given configuration
|
|
54
|
-
*
|
|
55
|
-
* @param {function} method - Trace function to call
|
|
56
|
-
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
57
|
-
* @param {object} options - Trace options, like id, kind, name and details
|
|
58
|
-
*/
|
|
59
|
-
const addEvent = ( method, workflowInfo, options ) => {
|
|
60
|
-
const { id, name, kind, details } = options;
|
|
61
|
-
const { workflowId, memo: { executionContext } } = workflowInfo;
|
|
62
|
-
method( { id, kind, name, details, parentId: workflowId, executionContext } );
|
|
63
|
-
};
|
|
64
|
-
|
|
65
5
|
// This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
|
|
66
6
|
export const sinks = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Workflow lifecycle sinks
|
|
10
|
+
*/
|
|
11
|
+
workflow: {
|
|
12
|
+
start: {
|
|
13
|
+
fn: ( workflowInfo, input ) => {
|
|
14
|
+
const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
|
|
15
|
+
messageBus.emit( BusEventType.WORKFLOW_START, { id, name } );
|
|
16
|
+
if ( executionContext ) { // filters out internal workflows
|
|
17
|
+
Tracing.addEventStart( { id, kind: ComponentType.WORKFLOW, name, details: input, parentId, executionContext } );
|
|
18
|
+
}
|
|
72
19
|
},
|
|
73
20
|
callDuringReplay: false
|
|
74
21
|
},
|
|
75
22
|
|
|
76
|
-
|
|
77
|
-
fn: ( workflowInfo,
|
|
78
|
-
|
|
79
|
-
|
|
23
|
+
end: {
|
|
24
|
+
fn: ( workflowInfo, output ) => {
|
|
25
|
+
const { workflowId: id, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
|
|
26
|
+
messageBus.emit( BusEventType.WORKFLOW_END, { id, name, duration: Date.now() - startTime.getTime() } );
|
|
27
|
+
if ( executionContext ) { // filters out internal workflows
|
|
28
|
+
Tracing.addEventEnd( { id, details: output, executionContext } );
|
|
29
|
+
}
|
|
80
30
|
},
|
|
81
31
|
callDuringReplay: false
|
|
82
32
|
},
|
|
83
33
|
|
|
84
|
-
|
|
34
|
+
error: {
|
|
85
35
|
fn: ( workflowInfo, error ) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
36
|
+
const { workflowId: id, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
|
|
37
|
+
messageBus.emit( BusEventType.WORKFLOW_ERROR, { id, name, error, duration: Date.now() - startTime.getTime() } );
|
|
38
|
+
if ( executionContext ) { // filters out internal workflows
|
|
39
|
+
Tracing.addEventError( { id, details: error, executionContext } );
|
|
40
|
+
}
|
|
89
41
|
},
|
|
90
42
|
callDuringReplay: false
|
|
91
|
-
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
92
45
|
|
|
93
|
-
|
|
94
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Generic trace sinks
|
|
48
|
+
*/
|
|
49
|
+
trace: {
|
|
50
|
+
start: {
|
|
51
|
+
fn: ( workflowInfo, { id, name, kind, details } ) => {
|
|
52
|
+
const { memo: { executionContext, parentId } } = workflowInfo;
|
|
53
|
+
Tracing.addEventStart( { id, kind, name, details, parentId, executionContext } );
|
|
54
|
+
},
|
|
95
55
|
callDuringReplay: false
|
|
96
56
|
},
|
|
97
57
|
|
|
98
|
-
|
|
99
|
-
fn: (
|
|
58
|
+
end: {
|
|
59
|
+
fn: ( workflowInfo, { id, details } ) => {
|
|
60
|
+
const { memo: { executionContext } } = workflowInfo;
|
|
61
|
+
Tracing.addEventEnd( { id, details, executionContext } );
|
|
62
|
+
},
|
|
100
63
|
callDuringReplay: false
|
|
101
64
|
},
|
|
102
65
|
|
|
103
|
-
|
|
104
|
-
fn: (
|
|
66
|
+
error: {
|
|
67
|
+
fn: ( workflowInfo, { id, details } ) => {
|
|
68
|
+
const { memo: { executionContext } } = workflowInfo;
|
|
69
|
+
Tracing.addEventError( { id, details, executionContext } );
|
|
70
|
+
},
|
|
105
71
|
callDuringReplay: false
|
|
106
|
-
|
|
107
72
|
}
|
|
108
73
|
}
|
|
109
74
|
};
|