@output.ai/core 0.3.2 → 0.3.3
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
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy, continueAsNew } from '@temporalio/workflow';
|
|
3
3
|
import { validateWorkflow } from './validations/static.js';
|
|
4
4
|
import { validateWithSchema } from './validations/runtime.js';
|
|
5
|
-
import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
|
|
5
|
+
import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
6
|
import { deepMerge, mergeActivityOptions, setMetadata } from '#utils';
|
|
7
7
|
import { FatalError, ValidationError } from '#errors';
|
|
8
8
|
|
|
@@ -93,47 +93,59 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
93
93
|
activityOptions: memo.activityOptions ?? activityOptions // Also preserve the original activity options
|
|
94
94
|
} );
|
|
95
95
|
|
|
96
|
-
//
|
|
97
|
-
|
|
96
|
+
// Run the internal activity to retrieve the workflow trace destinations (if it is root workflow, not nested)
|
|
97
|
+
const traceDestinations = isRoot ? ( await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( { startTime, workflowId, workflowName: name } ) ) : null;
|
|
98
|
+
const traceObject = { trace: { destinations: traceDestinations } };
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
|
|
103
|
-
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
|
|
100
|
+
try {
|
|
101
|
+
// validation comes after setting memo to have that info already set for interceptor even if validations fail
|
|
102
|
+
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
104
|
+
// binds the methods called in the code that Webpack loader will add, they will exposed via "this"
|
|
105
|
+
const output = await fn.call( {
|
|
106
|
+
invokeStep: async ( stepName, input, options ) => steps[`${name}#${stepName}`]( input, options ),
|
|
107
|
+
invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
|
|
108
|
+
invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Start a child workflow
|
|
112
|
+
*
|
|
113
|
+
* @param {string} childName
|
|
114
|
+
* @param {unknown} input
|
|
115
|
+
* @param {object} extra
|
|
116
|
+
* @param {boolean} extra.detached
|
|
117
|
+
* @param {import('@temporalio/workflow').ActivityOptions} extra.options
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
120
|
+
startWorkflow: async ( childName, input, extra = {} ) =>
|
|
121
|
+
executeChild( childName, {
|
|
122
|
+
args: input ? [ input ] : [],
|
|
123
|
+
workflowId: `${workflowId}-${childName}-${uuid4()}`,
|
|
124
|
+
parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
|
|
125
|
+
memo: {
|
|
126
|
+
executionContext,
|
|
127
|
+
parentId: workflowId,
|
|
128
|
+
// new configuration for activities of the child workflow, this will be omitted so it will use what that workflow have defined
|
|
129
|
+
...( extra?.options && { activityOptions: mergeActivityOptions( activityOptions, extra.options ) } )
|
|
130
|
+
}
|
|
131
|
+
} )
|
|
132
|
+
}, input, context );
|
|
133
|
+
|
|
134
|
+
validateWithSchema( outputSchema, output, `Workflow ${name} output` );
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
if ( isRoot ) {
|
|
137
|
+
// Append the trace info to the result of the workflow
|
|
138
|
+
return { output, ...traceObject };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return output;
|
|
142
|
+
} catch ( e ) {
|
|
143
|
+
// Append the trace info as metadata of the error, so it can be read by the interceptor.
|
|
144
|
+
if ( isRoot ) {
|
|
145
|
+
e[METADATA_ACCESS_SYMBOL] = { ...( e[METADATA_ACCESS_SYMBOL] ?? {} ), ...traceObject };
|
|
146
|
+
}
|
|
147
|
+
throw e;
|
|
148
|
+
}
|
|
137
149
|
};
|
|
138
150
|
|
|
139
151
|
setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
|
|
@@ -6,71 +6,76 @@ import { EOL } from 'node:os';
|
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const tempFilesTTL = 1000 * 60 * 60 * 24 * 7; // 1 week in milliseconds
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
const
|
|
11
|
+
// Retrieves the caller path from the standard args used to start workflows
|
|
12
|
+
const callerDir = process.argv[2];
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
const LOCAL_TRACE_LOG_PATH = join( LOCAL_PROJECT_ROOT_PATH, 'logs' );
|
|
16
|
-
|
|
17
|
-
// The path to the temporary trace logs
|
|
18
|
-
const TMP_TRACE_LOG_PATH = join( __dirname, 'temp', 'traces' );
|
|
14
|
+
const tempTraceFilesDir = join( __dirname, 'temp', 'traces' );
|
|
19
15
|
|
|
20
16
|
const accumulate = ( { entry, executionContext: { workflowId, startTime } } ) => {
|
|
21
|
-
const path = join(
|
|
17
|
+
const path = join( tempTraceFilesDir, `${startTime}_${workflowId}.trace` );
|
|
22
18
|
appendFileSync( path, JSON.stringify( entry ) + EOL, 'utf-8' );
|
|
23
19
|
return readFileSync( path, 'utf-8' ).split( EOL ).slice( 0, -1 ).map( v => JSON.parse( v ) );
|
|
24
20
|
};
|
|
25
21
|
|
|
26
|
-
const cleanupOldTempFiles = ( threshold = Date.now() -
|
|
27
|
-
readdirSync(
|
|
22
|
+
const cleanupOldTempFiles = ( threshold = Date.now() - tempFilesTTL ) =>
|
|
23
|
+
readdirSync( tempTraceFilesDir )
|
|
28
24
|
.filter( f => +f.split( '_' )[0] < threshold )
|
|
29
|
-
.forEach( f => rmSync( join(
|
|
25
|
+
.forEach( f => rmSync( join( tempTraceFilesDir, f ) ) );
|
|
30
26
|
|
|
31
27
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @returns {string} The host trace log path from HOST_TRACE_PATH env var, or local path as fallback
|
|
28
|
+
* Resolves the deep folder structure that stores a workflow trace.
|
|
29
|
+
* @param {string} workflowName - Name of the workflow
|
|
30
|
+
* @returns {string}
|
|
36
31
|
*/
|
|
37
|
-
const
|
|
38
|
-
return process.env.HOST_TRACE_PATH || LOCAL_TRACE_LOG_PATH;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Init this processor
|
|
43
|
-
*/
|
|
44
|
-
export const init = () => {
|
|
45
|
-
mkdirSync( TMP_TRACE_LOG_PATH, { recursive: true } );
|
|
46
|
-
cleanupOldTempFiles();
|
|
47
|
-
};
|
|
32
|
+
const resolveTraceFolder = workflowName => join( 'runs', workflowName );
|
|
48
33
|
|
|
49
34
|
/**
|
|
50
|
-
*
|
|
51
|
-
* Uses the project root path
|
|
35
|
+
* Resolves the local file system path for ALL file I/O operations (read/write)
|
|
36
|
+
* Uses the project root path
|
|
52
37
|
* @param {string} workflowName - The name of the workflow
|
|
53
38
|
* @returns {string} The local filesystem path for file operations
|
|
54
39
|
*/
|
|
55
|
-
const
|
|
56
|
-
return join( LOCAL_PROJECT_ROOT_PATH, 'logs', 'runs', workflowName );
|
|
57
|
-
};
|
|
40
|
+
const resolveIOPath = workflowName => join( callerDir, 'logs', resolveTraceFolder( workflowName ) );
|
|
58
41
|
|
|
59
42
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
43
|
+
* Resolves the file path to be reported as the trace destination.
|
|
44
|
+
*
|
|
45
|
+
* Considering that in containerized environments (e.g., Docker), the file path might differ from the host machine,
|
|
46
|
+
* this value takes in consideration the TRACE_HOST_PATH env variable instead of the local filesystem to mount
|
|
47
|
+
* the final file path.
|
|
48
|
+
*
|
|
49
|
+
* If the env variable is not present, it falls back to the same value used to write files locally.
|
|
50
|
+
*
|
|
62
51
|
* @param {string} workflowName - The name of the workflow
|
|
63
|
-
* @returns {string} The path to report
|
|
52
|
+
* @returns {string} The path to report, reflecting the actual filesystem
|
|
64
53
|
*/
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
const resolveReportPath = workflowName => process.env.TRACE_HOST_PATH ?
|
|
55
|
+
join( process.env.TRACE_HOST_PATH, resolveTraceFolder( workflowName ) ) :
|
|
56
|
+
resolveIOPath( workflowName );
|
|
68
57
|
|
|
69
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Builds the actual trace filename
|
|
60
|
+
*
|
|
61
|
+
* @param {object} options
|
|
62
|
+
* @param {number} options.startTime
|
|
63
|
+
* @param {string} options.workflowId
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
const buildTraceFilename = ( { startTime, workflowId } ) => {
|
|
70
67
|
const timestamp = new Date( startTime ).toISOString().replace( /[:T.]/g, '-' );
|
|
71
68
|
return `${timestamp}_${workflowId}.json`;
|
|
72
69
|
};
|
|
73
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Init this processor
|
|
73
|
+
*/
|
|
74
|
+
export const init = () => {
|
|
75
|
+
mkdirSync( tempTraceFilesDir, { recursive: true } );
|
|
76
|
+
cleanupOldTempFiles();
|
|
77
|
+
};
|
|
78
|
+
|
|
74
79
|
/**
|
|
75
80
|
* Execute this processor:
|
|
76
81
|
*
|
|
@@ -84,24 +89,23 @@ const buildOutputFileName = ( { startTime, workflowId } ) => {
|
|
|
84
89
|
export const exec = ( { entry, executionContext } ) => {
|
|
85
90
|
const { workflowId, workflowName, startTime } = executionContext;
|
|
86
91
|
const content = buildTraceTree( accumulate( { entry, executionContext } ) );
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const dir = getLocalOutputDir( workflowName );
|
|
90
|
-
const path = join( dir, buildOutputFileName( { startTime, workflowId } ) );
|
|
92
|
+
const dir = resolveIOPath( workflowName );
|
|
93
|
+
const path = join( dir, buildTraceFilename( { startTime, workflowId } ) );
|
|
91
94
|
|
|
92
95
|
mkdirSync( dir, { recursive: true } );
|
|
93
96
|
writeFileSync( path, JSON.stringify( content, undefined, 2 ) + EOL, 'utf-8' );
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
|
-
* Returns where the trace is saved as an absolute path
|
|
100
|
+
* Returns where the trace is saved as an absolute path.
|
|
101
|
+
*
|
|
102
|
+
* This uses the optional TRACE_HOST_PATH to return values relative to the host OS, not the container, if applicable.
|
|
103
|
+
*
|
|
98
104
|
* @param {object} args
|
|
99
105
|
* @param {string} args.startTime - The start time of the workflow
|
|
100
106
|
* @param {string} args.workflowId - The id of the workflow execution
|
|
101
107
|
* @param {string} args.workflowName - The name of the workflow
|
|
102
108
|
* @returns {string} The absolute path where the trace will be saved
|
|
103
109
|
*/
|
|
104
|
-
export const getDestination = ( { startTime, workflowId, workflowName } ) =>
|
|
105
|
-
|
|
106
|
-
return join( getReportOutputDir( workflowName ), buildOutputFileName( { workflowId, startTime } ) );
|
|
107
|
-
};
|
|
110
|
+
export const getDestination = ( { startTime, workflowId, workflowName } ) =>
|
|
111
|
+
join( resolveReportPath( workflowName ), buildTraceFilename( { workflowId, startTime } ) );
|
|
@@ -29,7 +29,7 @@ describe( 'tracing/processors/local', () => {
|
|
|
29
29
|
vi.clearAllMocks();
|
|
30
30
|
store.files.clear();
|
|
31
31
|
process.argv[2] = '/tmp/project';
|
|
32
|
-
delete process.env.
|
|
32
|
+
delete process.env.TRACE_HOST_PATH; // Clear TRACE_HOST_PATH for clean tests
|
|
33
33
|
} );
|
|
34
34
|
|
|
35
35
|
it( 'init(): creates temp dir and cleans up old files', async () => {
|
|
@@ -62,7 +62,7 @@ describe( 'tracing/processors/local', () => {
|
|
|
62
62
|
|
|
63
63
|
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 3 );
|
|
64
64
|
const [ writtenPath, content ] = writeFileSyncMock.mock.calls.at( -1 );
|
|
65
|
-
// Changed: Now uses process.cwd() + '/logs' fallback when
|
|
65
|
+
// Changed: Now uses process.cwd() + '/logs' fallback when TRACE_HOST_PATH not set
|
|
66
66
|
expect( writtenPath ).toMatch( /\/runs\/WF\// );
|
|
67
67
|
expect( JSON.parse( content.trim() ).count ).toBe( 3 );
|
|
68
68
|
} );
|
|
@@ -81,11 +81,11 @@ describe( 'tracing/processors/local', () => {
|
|
|
81
81
|
expect( destination ).toContain( '/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
|
|
82
82
|
} );
|
|
83
83
|
|
|
84
|
-
it( 'exec(): writes to container path regardless of
|
|
84
|
+
it( 'exec(): writes to container path regardless of TRACE_HOST_PATH', async () => {
|
|
85
85
|
const { exec, init } = await import( './index.js' );
|
|
86
86
|
|
|
87
|
-
// Set
|
|
88
|
-
process.env.
|
|
87
|
+
// Set TRACE_HOST_PATH to simulate Docker environment
|
|
88
|
+
process.env.TRACE_HOST_PATH = '/host/path/logs';
|
|
89
89
|
|
|
90
90
|
init();
|
|
91
91
|
|
|
@@ -97,16 +97,16 @@ describe( 'tracing/processors/local', () => {
|
|
|
97
97
|
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
98
98
|
const [ writtenPath ] = writeFileSyncMock.mock.calls.at( -1 );
|
|
99
99
|
|
|
100
|
-
// Should write to process.cwd()/logs, NOT to
|
|
100
|
+
// Should write to process.cwd()/logs, NOT to TRACE_HOST_PATH
|
|
101
101
|
expect( writtenPath ).not.toContain( '/host/path/logs' );
|
|
102
102
|
expect( writtenPath ).toMatch( /logs\/runs\/WF\// );
|
|
103
103
|
} );
|
|
104
104
|
|
|
105
|
-
it( 'getDestination(): returns
|
|
105
|
+
it( 'getDestination(): returns TRACE_HOST_PATH when set', async () => {
|
|
106
106
|
const { getDestination } = await import( './index.js' );
|
|
107
107
|
|
|
108
|
-
// Set
|
|
109
|
-
process.env.
|
|
108
|
+
// Set TRACE_HOST_PATH to simulate Docker environment
|
|
109
|
+
process.env.TRACE_HOST_PATH = '/host/path/logs';
|
|
110
110
|
|
|
111
111
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
112
112
|
const workflowId = 'workflow-id-123';
|
|
@@ -114,15 +114,15 @@ describe( 'tracing/processors/local', () => {
|
|
|
114
114
|
|
|
115
115
|
const destination = getDestination( { startTime, workflowId, workflowName } );
|
|
116
116
|
|
|
117
|
-
// Should return
|
|
117
|
+
// Should return TRACE_HOST_PATH-based path for reporting
|
|
118
118
|
expect( destination ).toBe( '/host/path/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
|
|
119
119
|
} );
|
|
120
120
|
|
|
121
121
|
it( 'separation of write and report paths works correctly', async () => {
|
|
122
122
|
const { exec, getDestination, init } = await import( './index.js' );
|
|
123
123
|
|
|
124
|
-
// Set
|
|
125
|
-
process.env.
|
|
124
|
+
// Set TRACE_HOST_PATH to simulate Docker environment
|
|
125
|
+
process.env.TRACE_HOST_PATH = '/Users/ben/project/logs';
|
|
126
126
|
|
|
127
127
|
init();
|
|
128
128
|
|
|
@@ -142,7 +142,7 @@ describe( 'tracing/processors/local', () => {
|
|
|
142
142
|
expect( writtenPath ).not.toContain( '/Users/ben/project' );
|
|
143
143
|
expect( writtenPath ).toMatch( /logs\/runs\/test-workflow\// );
|
|
144
144
|
|
|
145
|
-
// Verify report path uses
|
|
145
|
+
// Verify report path uses TRACE_HOST_PATH
|
|
146
146
|
expect( destination ).toBe( '/Users/ben/project/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
|
|
147
147
|
} );
|
|
148
148
|
} );
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew } from '@temporalio/workflow';
|
|
3
3
|
import { memoToHeaders } from '../sandboxed_utils.js';
|
|
4
4
|
import { mergeActivityOptions } from '#utils';
|
|
5
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
5
6
|
// this is a dynamic generated file with activity configs overwrites
|
|
6
7
|
import stepOptions from '../temp/__activity_options.js';
|
|
7
8
|
|
|
@@ -37,15 +38,28 @@ class WorkflowExecutionInterceptor {
|
|
|
37
38
|
sinks.trace.addWorkflowEventEnd( output );
|
|
38
39
|
return output;
|
|
39
40
|
} catch ( error ) {
|
|
40
|
-
/*
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/*
|
|
42
|
+
* When the error is a ContinueAsNew instance, it represents the point where the actual workflow code was
|
|
43
|
+
* delegated to another run. In this case the result in the traces will be the string below and
|
|
44
|
+
* a new trace file will be generated
|
|
45
|
+
*/
|
|
43
46
|
if ( error instanceof ContinueAsNew ) {
|
|
44
47
|
sinks.trace.addWorkflowEventEnd( '<continued_as_new>' );
|
|
45
48
|
throw error;
|
|
46
49
|
}
|
|
50
|
+
|
|
47
51
|
sinks.trace.addWorkflowEventError( error );
|
|
48
|
-
|
|
52
|
+
const failure = new ApplicationFailure( error.message, error.constructor.name );
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* If intercepted error has metadata, set it to .details property of Temporal's ApplicationFailure instance.
|
|
56
|
+
* This make it possible for this information be retrieved by Temporal's client instance.
|
|
57
|
+
* Ref: https://typescript.temporal.io/api/classes/common.ApplicationFailure#details
|
|
58
|
+
*/
|
|
59
|
+
if ( error[METADATA_ACCESS_SYMBOL] ) {
|
|
60
|
+
failure.details = [ error[METADATA_ACCESS_SYMBOL] ];
|
|
61
|
+
}
|
|
62
|
+
throw failure;
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
65
|
};
|