@output.ai/core 0.3.2 → 0.3.4
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 +3 -1
- package/src/interface/workflow.js +51 -39
- package/src/internal_activities/index.js +4 -1
- package/src/logger.d.ts +4 -0
- package/src/logger.js +56 -0
- package/src/tracing/processors/local/index.js +52 -48
- package/src/tracing/processors/local/index.spec.js +13 -13
- package/src/tracing/trace_engine.js +5 -2
- package/src/worker/catalog_workflow/index.js +4 -1
- package/src/worker/index.js +18 -15
- package/src/worker/interceptors/workflow.js +18 -4
- package/src/worker/loader.js +6 -3
- package/src/worker/sinks.js +57 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@output.ai/core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"redis": "5.8.3",
|
|
40
40
|
"stacktrace-parser": "0.1.11",
|
|
41
41
|
"undici": "7.18.2",
|
|
42
|
+
"winston": "3.17.0",
|
|
42
43
|
"zod": "4.1.12"
|
|
43
44
|
},
|
|
44
45
|
"license": "Apache-2.0",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"imports": {
|
|
49
50
|
"#consts": "./src/consts.js",
|
|
50
51
|
"#errors": "./src/errors.js",
|
|
52
|
+
"#logger": "./src/logger.js",
|
|
51
53
|
"#utils": "./src/utils/index.js",
|
|
52
54
|
"#tracing": "./src/tracing/internal_interface.js",
|
|
53
55
|
"#async_storage": "./src/async_storage.js",
|
|
@@ -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 } );
|
|
@@ -4,6 +4,9 @@ import { setMetadata, isStringboolTrue, serializeFetchResponse, serializeBodyAnd
|
|
|
4
4
|
import { ComponentType } from '#consts';
|
|
5
5
|
import * as localProcessor from '../tracing/processors/local/index.js';
|
|
6
6
|
import * as s3Processor from '../tracing/processors/s3/index.js';
|
|
7
|
+
import { createChildLogger } from '#logger';
|
|
8
|
+
|
|
9
|
+
const log = createChildLogger( 'HttpClient' );
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Send a HTTP request.
|
|
@@ -42,7 +45,7 @@ export const sendHttpRequest = async ( { url, method, payload = undefined, heade
|
|
|
42
45
|
}
|
|
43
46
|
} )();
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
log.info( 'HTTP request completed', { url, method, status: response.status, statusText: response.statusText } );
|
|
46
49
|
|
|
47
50
|
if ( !response.ok ) {
|
|
48
51
|
throw new FatalError( `${method} ${url} ${response.status}` );
|
package/src/logger.d.ts
ADDED
package/src/logger.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
4
|
+
|
|
5
|
+
const levels = {
|
|
6
|
+
error: 0,
|
|
7
|
+
warn: 1,
|
|
8
|
+
info: 2,
|
|
9
|
+
http: 3,
|
|
10
|
+
debug: 4
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Format metadata as friendly JSON: "{ name: "foo", count: 5 }"
|
|
14
|
+
const formatMeta = obj => {
|
|
15
|
+
const entries = Object.entries( obj );
|
|
16
|
+
if ( !entries.length ) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
return ' { ' + entries.map( ( [ k, v ] ) => `${k}: ${JSON.stringify( v )}` ).join( ', ' ) + ' }';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Development format: colorized with namespace prefix
|
|
23
|
+
const devFormat = winston.format.combine(
|
|
24
|
+
winston.format.colorize(),
|
|
25
|
+
winston.format.printf( ( { level, message, namespace, service: _, environment: __, ...rest } ) => {
|
|
26
|
+
const ns = namespace ? `{Core.${namespace}}` : '{Core}';
|
|
27
|
+
const meta = formatMeta( rest );
|
|
28
|
+
return `[${level}] ${ns} ${message}${meta}`;
|
|
29
|
+
} )
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Production format: structured JSON
|
|
33
|
+
const prodFormat = winston.format.combine(
|
|
34
|
+
winston.format.timestamp( { format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' } ),
|
|
35
|
+
winston.format.errors( { stack: true } ),
|
|
36
|
+
winston.format.json()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const logger = winston.createLogger( {
|
|
40
|
+
levels,
|
|
41
|
+
level: isProduction ? 'info' : 'debug',
|
|
42
|
+
format: isProduction ? prodFormat : devFormat,
|
|
43
|
+
defaultMeta: {
|
|
44
|
+
service: 'output-worker',
|
|
45
|
+
environment: process.env.NODE_ENV || 'development'
|
|
46
|
+
},
|
|
47
|
+
transports: [ new winston.transports.Console() ]
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a child logger with a specific namespace
|
|
52
|
+
*
|
|
53
|
+
* @param {string} namespace - The namespace for this logger (e.g., 'Scanner', 'Tracing')
|
|
54
|
+
* @returns {winston.Logger} Child logger instance with namespace metadata
|
|
55
|
+
*/
|
|
56
|
+
export const createChildLogger = namespace => logger.child( { namespace } );
|
|
@@ -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
|
} );
|
|
@@ -5,6 +5,9 @@ import { isStringboolTrue } from '#utils';
|
|
|
5
5
|
import * as localProcessor from './processors/local/index.js';
|
|
6
6
|
import * as s3Processor from './processors/s3/index.js';
|
|
7
7
|
import { ComponentType } from '#consts';
|
|
8
|
+
import { createChildLogger } from '#logger';
|
|
9
|
+
|
|
10
|
+
const log = createChildLogger( 'Tracing' );
|
|
8
11
|
|
|
9
12
|
const traceBus = new EventEmitter();
|
|
10
13
|
const processors = [
|
|
@@ -32,7 +35,7 @@ export const init = async () => {
|
|
|
32
35
|
try {
|
|
33
36
|
await p.exec( ...args );
|
|
34
37
|
} catch ( error ) {
|
|
35
|
-
|
|
38
|
+
log.error( 'Processor execution error', { processor: p.name, error: error.message, stack: error.stack } );
|
|
36
39
|
}
|
|
37
40
|
} );
|
|
38
41
|
}
|
|
@@ -44,7 +47,7 @@ export const init = async () => {
|
|
|
44
47
|
const serializeDetails = details => details instanceof Error ? serializeError( details ) : details;
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
|
-
* Creates a new trace event phase and
|
|
50
|
+
* Creates a new trace event phase and sends it to be written
|
|
48
51
|
*
|
|
49
52
|
* @param {string} phase - The phase
|
|
50
53
|
* @param {object} fields - All the trace fields
|
|
@@ -2,6 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
4
|
import { Catalog, CatalogActivity, CatalogWorkflow } from './catalog.js';
|
|
5
|
+
import { createChildLogger } from '#logger';
|
|
6
|
+
|
|
7
|
+
const log = createChildLogger( 'Catalog' );
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Converts a Zod schema to JSON Schema format.
|
|
@@ -17,7 +20,7 @@ const convertToJsonSchema = schema => {
|
|
|
17
20
|
try {
|
|
18
21
|
return z.toJSONSchema( schema );
|
|
19
22
|
} catch ( error ) {
|
|
20
|
-
|
|
23
|
+
log.warn( 'Invalid schema provided (expected Zod schema)', { error: error.message } );
|
|
21
24
|
return null;
|
|
22
25
|
}
|
|
23
26
|
};
|
package/src/worker/index.js
CHANGED
|
@@ -9,31 +9,34 @@ import { init as initTracing } from '#tracing';
|
|
|
9
9
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
10
10
|
import { webpackConfigHook } from './bundler_options.js';
|
|
11
11
|
import { initInterceptors } from './interceptors.js';
|
|
12
|
+
import { createChildLogger } from '#logger';
|
|
13
|
+
|
|
14
|
+
const log = createChildLogger( 'Worker' );
|
|
12
15
|
|
|
13
16
|
// Get caller directory from command line arguments
|
|
14
17
|
const callerDir = process.argv[2];
|
|
15
18
|
|
|
16
19
|
( async () => {
|
|
17
|
-
|
|
20
|
+
log.info( 'Loading workflows...', { callerDir } );
|
|
18
21
|
const workflows = await loadWorkflows( callerDir );
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
log.info( 'Loading activities...', { callerDir } );
|
|
21
24
|
const activities = await loadActivities( callerDir, workflows );
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
log.info( 'Creating worker entry point...' );
|
|
24
27
|
const workflowsPath = createWorkflowsEntryPoint( workflows );
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
log.info( 'Initializing tracing...' );
|
|
27
30
|
await initTracing();
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
log.info( 'Creating workflows catalog...' );
|
|
30
33
|
const catalog = createCatalog( { workflows, activities } );
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
//
|
|
35
|
+
log.info( 'Connecting Temporal...' );
|
|
36
|
+
// Enable TLS when connecting to remote Temporal (API key present)
|
|
34
37
|
const connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey } );
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
log.info( 'Creating worker...' );
|
|
37
40
|
const worker = await Worker.create( {
|
|
38
41
|
connection,
|
|
39
42
|
namespace,
|
|
@@ -47,7 +50,7 @@ const callerDir = process.argv[2];
|
|
|
47
50
|
bundlerOptions: { webpackConfigHook }
|
|
48
51
|
} );
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
log.info( 'Starting catalog workflow...' );
|
|
51
54
|
await new Client( { connection, namespace } ).workflow.start( WORKFLOW_CATALOG, {
|
|
52
55
|
taskQueue,
|
|
53
56
|
workflowId: catalogId, // use the name of the task queue as the catalog name, ensuring uniqueness
|
|
@@ -55,7 +58,7 @@ const callerDir = process.argv[2];
|
|
|
55
58
|
args: [ catalog ]
|
|
56
59
|
} );
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
log.info( 'Running worker...' );
|
|
59
62
|
|
|
60
63
|
// FORCE_QUIT_GRACE_MS delays the second instance of a shutdown command.
|
|
61
64
|
// If running output-worker directly with npx, 2 signals are recieved in
|
|
@@ -69,12 +72,12 @@ const callerDir = process.argv[2];
|
|
|
69
72
|
if ( elapsed < FORCE_QUIT_GRACE_MS ) {
|
|
70
73
|
return; // ignore rapid duplicate signals
|
|
71
74
|
}
|
|
72
|
-
|
|
75
|
+
log.warn( 'Force quitting...' );
|
|
73
76
|
process.exit( 1 );
|
|
74
77
|
}
|
|
75
78
|
state.isShuttingDown = true;
|
|
76
79
|
state.shutdownStartedAt = Date.now();
|
|
77
|
-
|
|
80
|
+
log.info( 'Shutting down...', { signal } );
|
|
78
81
|
worker.shutdown();
|
|
79
82
|
};
|
|
80
83
|
|
|
@@ -82,13 +85,13 @@ const callerDir = process.argv[2];
|
|
|
82
85
|
process.on( 'SIGINT', () => shutdown( 'SIGINT' ) );
|
|
83
86
|
|
|
84
87
|
await worker.run();
|
|
85
|
-
|
|
88
|
+
log.info( 'Worker stopped.' );
|
|
86
89
|
|
|
87
90
|
await connection.close();
|
|
88
|
-
|
|
91
|
+
log.info( 'Connection closed.' );
|
|
89
92
|
|
|
90
93
|
process.exit( 0 );
|
|
91
94
|
} )().catch( error => {
|
|
92
|
-
|
|
95
|
+
log.error( 'Fatal error', { message: error.message, stack: error.stack } );
|
|
93
96
|
process.exit( 1 );
|
|
94
97
|
} );
|
|
@@ -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
|
};
|
package/src/worker/loader.js
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
WORKFLOW_CATALOG,
|
|
13
13
|
ACTIVITY_GET_TRACE_DESTINATIONS
|
|
14
14
|
} from '#consts';
|
|
15
|
+
import { createChildLogger } from '#logger';
|
|
16
|
+
|
|
17
|
+
const log = createChildLogger( 'Scanner' );
|
|
15
18
|
|
|
16
19
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
17
20
|
|
|
@@ -64,7 +67,7 @@ export async function loadActivities( rootDir, workflows ) {
|
|
|
64
67
|
for ( const { path: workflowPath, name: workflowName } of workflows ) {
|
|
65
68
|
const dir = dirname( workflowPath );
|
|
66
69
|
for await ( const { fn, metadata, path } of importComponents( dir, Object.values( activityMatchersBuilder( dir ) ) ) ) {
|
|
67
|
-
|
|
70
|
+
log.info( 'Component loaded', { type: metadata.type, name: metadata.name, path, workflow: workflowName } );
|
|
68
71
|
// Activities loaded from a workflow path will use the workflow name as a namespace, which is unique across the platform, avoiding collision
|
|
69
72
|
const activityKey = generateActivityKey( { namespace: workflowName, activityName: metadata.name } );
|
|
70
73
|
activities[activityKey] = fn;
|
|
@@ -75,7 +78,7 @@ export async function loadActivities( rootDir, workflows ) {
|
|
|
75
78
|
|
|
76
79
|
// Load shared activities/evaluators
|
|
77
80
|
for await ( const { fn, metadata, path } of importComponents( rootDir, [ staticMatchers.sharedStepsDir, staticMatchers.sharedEvaluatorsDir ] ) ) {
|
|
78
|
-
|
|
81
|
+
log.info( 'Shared component loaded', { type: metadata.type, name: metadata.name, path } );
|
|
79
82
|
// The namespace for shared activities is fixed
|
|
80
83
|
const activityKey = generateActivityKey( { namespace: SHARED_STEP_PREFIX, activityName: metadata.name } );
|
|
81
84
|
activities[activityKey] = fn;
|
|
@@ -105,7 +108,7 @@ export async function loadWorkflows( rootDir ) {
|
|
|
105
108
|
if ( staticMatchers.workflowPathHasShared( path ) ) {
|
|
106
109
|
throw new Error( 'Workflow directory can\'t be named "shared"' );
|
|
107
110
|
}
|
|
108
|
-
|
|
111
|
+
log.info( 'Workflow loaded', { name: metadata.name, path } );
|
|
109
112
|
workflows.push( { ...metadata, path } );
|
|
110
113
|
}
|
|
111
114
|
return workflows;
|
package/src/worker/sinks.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
2
2
|
import { addEventStart, addEventEnd, addEventError } from '#tracing';
|
|
3
|
+
import { createChildLogger } from '#logger';
|
|
4
|
+
|
|
5
|
+
const log = createChildLogger( 'Worker' );
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Start a workflow trace event
|
|
6
9
|
*
|
|
7
10
|
* @param {function} method - Trace function to call
|
|
8
11
|
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
9
|
-
* @param {object} details -
|
|
12
|
+
* @param {object} details - The details to attach to the event
|
|
10
13
|
*/
|
|
11
14
|
const addWorkflowEvent = ( method, workflowInfo, details ) => {
|
|
12
15
|
const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
|
|
@@ -16,6 +19,47 @@ const addWorkflowEvent = ( method, workflowInfo, details ) => {
|
|
|
16
19
|
method( { id, kind: 'workflow', name, details, parentId, executionContext } );
|
|
17
20
|
};
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Log workflow start
|
|
24
|
+
*
|
|
25
|
+
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
26
|
+
*/
|
|
27
|
+
const logWorkflowStart = workflowInfo => {
|
|
28
|
+
const { workflowId, workflowType: workflowName } = workflowInfo;
|
|
29
|
+
if ( workflowName === WORKFLOW_CATALOG ) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
log.info( 'Workflow started', { workflowName, workflowId } );
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Log workflow completion with duration
|
|
37
|
+
*
|
|
38
|
+
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
39
|
+
*/
|
|
40
|
+
const logWorkflowEnd = workflowInfo => {
|
|
41
|
+
const { workflowId, workflowType: workflowName, startTime } = workflowInfo;
|
|
42
|
+
if ( workflowName === WORKFLOW_CATALOG ) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const durationMs = Date.now() - startTime.getTime();
|
|
46
|
+
log.info( 'Workflow completed', { workflowName, workflowId, durationMs } );
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Log workflow failure with duration
|
|
51
|
+
*
|
|
52
|
+
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
53
|
+
*/
|
|
54
|
+
const logWorkflowError = workflowInfo => {
|
|
55
|
+
const { workflowId, workflowType: workflowName, startTime } = workflowInfo;
|
|
56
|
+
if ( workflowName === WORKFLOW_CATALOG ) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const durationMs = Date.now() - startTime.getTime();
|
|
60
|
+
log.error( 'Workflow failed', { workflowName, workflowId, durationMs } );
|
|
61
|
+
};
|
|
62
|
+
|
|
19
63
|
/**
|
|
20
64
|
* Start a trace event with given configuration
|
|
21
65
|
*
|
|
@@ -33,17 +77,26 @@ const addEvent = ( method, workflowInfo, options ) => {
|
|
|
33
77
|
export const sinks = {
|
|
34
78
|
trace: {
|
|
35
79
|
addWorkflowEventStart: {
|
|
36
|
-
fn: ( ...
|
|
80
|
+
fn: ( workflowInfo, ...rest ) => {
|
|
81
|
+
logWorkflowStart( workflowInfo );
|
|
82
|
+
addWorkflowEvent( addEventStart, workflowInfo, ...rest );
|
|
83
|
+
},
|
|
37
84
|
callDuringReplay: false
|
|
38
85
|
},
|
|
39
86
|
|
|
40
87
|
addWorkflowEventEnd: {
|
|
41
|
-
fn: ( ...
|
|
88
|
+
fn: ( workflowInfo, ...rest ) => {
|
|
89
|
+
logWorkflowEnd( workflowInfo );
|
|
90
|
+
addWorkflowEvent( addEventEnd, workflowInfo, ...rest );
|
|
91
|
+
},
|
|
42
92
|
callDuringReplay: false
|
|
43
93
|
},
|
|
44
94
|
|
|
45
95
|
addWorkflowEventError: {
|
|
46
|
-
fn: ( ...
|
|
96
|
+
fn: ( workflowInfo, ...rest ) => {
|
|
97
|
+
logWorkflowError( workflowInfo );
|
|
98
|
+
addWorkflowEvent( addEventError, workflowInfo, ...rest );
|
|
99
|
+
},
|
|
47
100
|
callDuringReplay: false
|
|
48
101
|
},
|
|
49
102
|
|