@outputai/core 0.6.0 → 0.6.1-dev.aab2335.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/activity_integration/context.d.ts +5 -9
- package/src/activity_integration/context.js +5 -4
- package/src/activity_integration/context.spec.js +10 -15
- package/src/activity_integration/events.d.ts +2 -4
- package/src/activity_integration/events.js +8 -3
- package/src/activity_integration/events.spec.js +58 -29
- package/src/bus.js +18 -9
- package/src/bus.spec.js +30 -0
- package/src/hooks/index.d.ts +112 -58
- package/src/hooks/index.js +15 -12
- package/src/hooks/index.spec.js +60 -32
- package/src/interface/workflow.js +19 -35
- package/src/interface/workflow.spec.js +104 -15
- package/src/internal_activities/index.js +3 -3
- package/src/internal_activities/index.spec.js +31 -1
- package/src/internal_utils/temporal_context.js +12 -0
- package/src/internal_utils/temporal_context.spec.ts +83 -0
- package/src/internal_utils/trace_info.js +21 -0
- package/src/internal_utils/trace_info.spec.js +47 -0
- package/src/internal_utils/workflow_context.js +29 -0
- package/src/internal_utils/workflow_context.spec.js +46 -0
- package/src/logger/development.js +61 -0
- package/src/logger/development.spec.js +70 -0
- package/src/logger/index.js +14 -0
- package/src/logger/index.spec.js +27 -0
- package/src/logger/production.js +15 -0
- package/src/logger/production.spec.js +52 -0
- package/src/tracing/internal_interface.js +4 -4
- package/src/tracing/processors/local/index.js +21 -26
- package/src/tracing/processors/local/index.spec.js +39 -45
- package/src/tracing/processors/s3/index.js +13 -23
- package/src/tracing/processors/s3/index.spec.js +33 -26
- package/src/tracing/trace_attribute.js +0 -1
- package/src/tracing/trace_engine.js +8 -12
- package/src/tracing/trace_engine.spec.js +31 -27
- package/src/worker/configs.js +2 -0
- package/src/worker/configs.spec.js +25 -0
- package/src/worker/index.js +4 -1
- package/src/worker/index.spec.js +4 -0
- package/src/worker/interceptors/activity.js +31 -29
- package/src/worker/interceptors/activity.spec.js +58 -26
- package/src/worker/interceptors/workflow.js +7 -2
- package/src/worker/interceptors/workflow.spec.js +42 -6
- package/src/worker/log_hooks.js +35 -46
- package/src/worker/log_hooks.spec.js +43 -46
- package/src/worker/setup_telemetry.js +19 -0
- package/src/worker/setup_telemetry.spec.js +80 -0
- package/src/worker/sinks.js +24 -24
- package/src/interface/workflow_context.js +0 -33
- package/src/logger.js +0 -73
|
@@ -7,7 +7,7 @@ import { JsonStreamStringify } from 'json-stream-stringify';
|
|
|
7
7
|
|
|
8
8
|
const log = createChildLogger( 'S3 Processor' );
|
|
9
9
|
|
|
10
|
-
const createRedisKey =
|
|
10
|
+
const createRedisKey = runId => `traces/${runId}`;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Add new entry to list of entries
|
|
@@ -44,17 +44,14 @@ const bustEntries = async key => {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Return the S3 key for the trace file
|
|
47
|
-
* @param {object}
|
|
48
|
-
* @param {number} args.startTime
|
|
49
|
-
* @param {string} args.workflowId
|
|
50
|
-
* @param {string} args.workflowName
|
|
47
|
+
* @param {object} traceInfo
|
|
51
48
|
* @returns
|
|
52
49
|
*/
|
|
53
|
-
const getS3Key = ( { startTime, workflowId,
|
|
50
|
+
const getS3Key = ( { startTime, workflowId, workflowType } ) => {
|
|
54
51
|
const isoDate = new Date( startTime ).toISOString();
|
|
55
52
|
const [ year, month, day ] = isoDate.split( /\D/, 3 );
|
|
56
53
|
const timeStamp = isoDate.replace( /[:T.]/g, '-' );
|
|
57
|
-
return `${
|
|
54
|
+
return `${workflowType}/${year}/${month}/${day}/${timeStamp}_${workflowId}.json`;
|
|
58
55
|
};
|
|
59
56
|
|
|
60
57
|
/**
|
|
@@ -70,19 +67,19 @@ export const init = async () => {
|
|
|
70
67
|
*
|
|
71
68
|
* Appends each trace entry to Redis.
|
|
72
69
|
*
|
|
73
|
-
* When the root workflow
|
|
70
|
+
* When the root workflow finishes or errors, builds the trace tree and uploads it to S3.
|
|
74
71
|
*
|
|
75
72
|
* @param {object} args
|
|
76
73
|
* @param {object} args.entry - The trace entry to append
|
|
77
|
-
* @param {object} args.
|
|
74
|
+
* @param {object} args.traceInfo - Trace information object
|
|
78
75
|
*/
|
|
79
|
-
export const exec = async ( { entry,
|
|
80
|
-
const {
|
|
81
|
-
const cacheKey = createRedisKey(
|
|
76
|
+
export const exec = async ( { entry, traceInfo } ) => {
|
|
77
|
+
const { workflowId, runId } = traceInfo;
|
|
78
|
+
const cacheKey = createRedisKey( runId );
|
|
82
79
|
|
|
83
80
|
await addEntry( entry, cacheKey );
|
|
84
81
|
|
|
85
|
-
const isRootWorkflowEnd = entry.id ===
|
|
82
|
+
const isRootWorkflowEnd = entry.id === runId && entry.action !== 'start';
|
|
86
83
|
if ( !isRootWorkflowEnd ) {
|
|
87
84
|
return;
|
|
88
85
|
}
|
|
@@ -100,20 +97,13 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
100
97
|
return;
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
await upload( {
|
|
104
|
-
key: getS3Key( { workflowId, workflowName, startTime } ),
|
|
105
|
-
content: new JsonStreamStringify( content )
|
|
106
|
-
} );
|
|
100
|
+
await upload( { key: getS3Key( traceInfo ), content: new JsonStreamStringify( content ) } );
|
|
107
101
|
await bustEntries( cacheKey );
|
|
108
102
|
};
|
|
109
103
|
|
|
110
104
|
/**
|
|
111
105
|
* Returns where the trace is saved
|
|
112
|
-
* @param {object}
|
|
113
|
-
* @param {string} executionContext.startTime - The start time of the workflow
|
|
114
|
-
* @param {string} executionContext.workflowId - The id of the workflow execution
|
|
115
|
-
* @param {string} executionContext.workflowName - The name of the workflow
|
|
106
|
+
* @param {object} traceInfo - Trace information object
|
|
116
107
|
* @returns {string} The S3 url of the trace file
|
|
117
108
|
*/
|
|
118
|
-
export const getDestination =
|
|
119
|
-
`https://${getVars().remoteS3Bucket}.s3.amazonaws.com/${getS3Key( { workflowId, workflowName, startTime } )}`;
|
|
109
|
+
export const getDestination = traceInfo => `https://${getVars().remoteS3Bucket}.s3.amazonaws.com/${getS3Key( traceInfo )}`;
|
|
@@ -53,6 +53,14 @@ const streamToString = async stream => {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
describe( 'tracing/processors/s3', () => {
|
|
56
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
57
|
+
const traceInfo = {
|
|
58
|
+
workflowId: 'id1',
|
|
59
|
+
runId: 'run-1',
|
|
60
|
+
workflowType: 'WF',
|
|
61
|
+
startTime
|
|
62
|
+
};
|
|
63
|
+
|
|
56
64
|
beforeEach( () => {
|
|
57
65
|
vi.useFakeTimers();
|
|
58
66
|
vi.clearAllMocks();
|
|
@@ -72,24 +80,30 @@ describe( 'tracing/processors/s3', () => {
|
|
|
72
80
|
|
|
73
81
|
it( 'exec(): accumulates via redis, uploads only on root workflow end', async () => {
|
|
74
82
|
const { exec } = await import( './index.js' );
|
|
75
|
-
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
76
|
-
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
77
83
|
|
|
78
84
|
redisMulti.exec.mockResolvedValue( [] );
|
|
79
85
|
|
|
80
|
-
const workflowStart = { id: '
|
|
81
|
-
const activityStart = {
|
|
82
|
-
|
|
86
|
+
const workflowStart = { id: 'run-1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
|
|
87
|
+
const activityStart = {
|
|
88
|
+
id: 'act-1',
|
|
89
|
+
name: 'DoSomething',
|
|
90
|
+
kind: 'step',
|
|
91
|
+
parentId: 'run-1',
|
|
92
|
+
action: 'start',
|
|
93
|
+
details: {},
|
|
94
|
+
timestamp: startTime + 1
|
|
95
|
+
};
|
|
96
|
+
const workflowEnd = { id: 'run-1', action: 'end', details: { ok: true }, timestamp: startTime + 2 };
|
|
83
97
|
zRangeMock.mockResolvedValue( [
|
|
84
98
|
JSON.stringify( workflowStart ),
|
|
85
99
|
JSON.stringify( activityStart ),
|
|
86
100
|
JSON.stringify( workflowEnd )
|
|
87
101
|
] );
|
|
88
102
|
|
|
89
|
-
await exec( {
|
|
90
|
-
await exec( {
|
|
91
|
-
// Root end: id matches
|
|
92
|
-
const endPromise = exec( {
|
|
103
|
+
await exec( { traceInfo, entry: workflowStart } );
|
|
104
|
+
await exec( { traceInfo, entry: activityStart } );
|
|
105
|
+
// Root end: id matches runId and not start — triggers the 10s delay before upload
|
|
106
|
+
const endPromise = exec( { traceInfo, entry: workflowEnd } );
|
|
93
107
|
await vi.advanceTimersByTimeAsync( 10_000 );
|
|
94
108
|
await endPromise;
|
|
95
109
|
|
|
@@ -101,14 +115,13 @@ describe( 'tracing/processors/s3', () => {
|
|
|
101
115
|
expect( key ).toMatch( /^WF\/2020\/01\/02\// );
|
|
102
116
|
expect( JSON.parse( await streamToString( content ) ).count ).toBe( 3 );
|
|
103
117
|
expect( delMock ).toHaveBeenCalledTimes( 1 );
|
|
104
|
-
expect( delMock ).toHaveBeenCalledWith( 'traces/
|
|
118
|
+
expect( delMock ).toHaveBeenCalledWith( 'traces/run-1' );
|
|
105
119
|
} );
|
|
106
120
|
|
|
107
121
|
it( 'getDestination(): returns S3 URL using bucket and key from getVars', async () => {
|
|
108
122
|
getVarsMock.mockReturnValue( { remoteS3Bucket: 'my-bucket', redisIncompleteWorkflowsTTL: 3600, traceUploadDelayMs: 10_000 } );
|
|
109
123
|
const { getDestination } = await import( './index.js' );
|
|
110
|
-
const
|
|
111
|
-
const url = getDestination( { workflowId: 'id1', workflowName: 'WF', startTime } );
|
|
124
|
+
const url = getDestination( traceInfo );
|
|
112
125
|
expect( getVarsMock ).toHaveBeenCalled();
|
|
113
126
|
expect( url ).toBe(
|
|
114
127
|
'https://my-bucket.s3.amazonaws.com/WF/2020/01/02/2020-01-02-03-04-05-678Z_id1.json'
|
|
@@ -117,36 +130,32 @@ describe( 'tracing/processors/s3', () => {
|
|
|
117
130
|
|
|
118
131
|
it( 'exec(): sets expiry on the redis key for each entry', async () => {
|
|
119
132
|
const { exec } = await import( './index.js' );
|
|
120
|
-
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
121
|
-
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
122
133
|
|
|
123
134
|
redisMulti.exec.mockResolvedValue( [] );
|
|
124
135
|
const workflowStart = {
|
|
125
|
-
kind: 'workflow', id: '
|
|
136
|
+
kind: 'workflow', id: 'run-1', name: 'WF', parentId: undefined, action: 'start', details: {}, timestamp: startTime
|
|
126
137
|
};
|
|
127
138
|
zRangeMock.mockResolvedValue( [ JSON.stringify( workflowStart ) ] );
|
|
128
139
|
|
|
129
|
-
await exec( {
|
|
140
|
+
await exec( { traceInfo, entry: workflowStart } );
|
|
130
141
|
|
|
131
142
|
expect( redisMulti.expire ).toHaveBeenCalledTimes( 1 );
|
|
132
|
-
expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/
|
|
143
|
+
expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/run-1', 3600 );
|
|
133
144
|
} );
|
|
134
145
|
|
|
135
146
|
it( 'exec(): does not treat a non-root end (e.g. step without parentId) as root workflow end — regression for wrong root detection', async () => {
|
|
136
147
|
const { exec } = await import( './index.js' );
|
|
137
|
-
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
138
|
-
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
139
148
|
|
|
140
149
|
redisMulti.exec.mockResolvedValue( [] );
|
|
141
|
-
const workflowStart = { id: '
|
|
150
|
+
const workflowStart = { id: 'run-1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
|
|
142
151
|
const stepEndNoParent = { id: 'step-1', action: 'end', details: { done: true }, timestamp: startTime + 1 };
|
|
143
152
|
zRangeMock.mockResolvedValue( [
|
|
144
153
|
JSON.stringify( workflowStart ),
|
|
145
154
|
JSON.stringify( stepEndNoParent )
|
|
146
155
|
] );
|
|
147
156
|
|
|
148
|
-
await exec( {
|
|
149
|
-
await exec( {
|
|
157
|
+
await exec( { traceInfo, entry: workflowStart } );
|
|
158
|
+
await exec( { traceInfo, entry: stepEndNoParent } );
|
|
150
159
|
|
|
151
160
|
expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 2 );
|
|
152
161
|
expect( buildTraceTreeMock ).not.toHaveBeenCalled();
|
|
@@ -156,17 +165,15 @@ describe( 'tracing/processors/s3', () => {
|
|
|
156
165
|
|
|
157
166
|
it( 'exec(): when buildTraceTree returns null (incomplete tree), does not upload or bust cache', async () => {
|
|
158
167
|
const { exec } = await import( './index.js' );
|
|
159
|
-
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
160
|
-
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
161
168
|
|
|
162
169
|
redisMulti.exec.mockResolvedValue( [] );
|
|
163
170
|
const workflowEnd = {
|
|
164
|
-
kind: 'workflow', id: '
|
|
171
|
+
kind: 'workflow', id: 'run-1', name: 'WF', parentId: undefined, action: 'end', details: {}, timestamp: startTime
|
|
165
172
|
};
|
|
166
173
|
zRangeMock.mockResolvedValue( [ JSON.stringify( workflowEnd ) ] );
|
|
167
174
|
buildTraceTreeMock.mockReturnValueOnce( null );
|
|
168
175
|
|
|
169
|
-
const endPromise = exec( {
|
|
176
|
+
const endPromise = exec( { traceInfo, entry: workflowEnd } );
|
|
170
177
|
await vi.advanceTimersByTimeAsync( 10_000 );
|
|
171
178
|
await endPromise;
|
|
172
179
|
|
|
@@ -32,16 +32,12 @@ const processors = [
|
|
|
32
32
|
/**
|
|
33
33
|
* Returns the destinations for a given execution context
|
|
34
34
|
*
|
|
35
|
-
* @param {object}
|
|
36
|
-
* @param {string} executionContext.startTime
|
|
37
|
-
* @param {string} executionContext.workflowId
|
|
38
|
-
* @param {string} executionContext.workflowName
|
|
39
|
-
* @param {boolean} executionContext.disableTrace
|
|
35
|
+
* @param {object} traceInfo - The trace information object
|
|
40
36
|
* @returns {object} A trace destinations object: { [dest-name]: 'path' }
|
|
41
37
|
*/
|
|
42
|
-
export const getDestinations =
|
|
38
|
+
export const getDestinations = traceInfo =>
|
|
43
39
|
processors.reduce( ( o, p ) =>
|
|
44
|
-
Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !
|
|
40
|
+
Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !traceInfo.disableTrace ? p.getDestination( traceInfo ) : null } )
|
|
45
41
|
, {} );
|
|
46
42
|
|
|
47
43
|
/**
|
|
@@ -72,11 +68,11 @@ const serializeDetails = details => details instanceof Error ? serializeError( d
|
|
|
72
68
|
* @param {object} fields - All the trace fields
|
|
73
69
|
* @returns {void}
|
|
74
70
|
*/
|
|
75
|
-
export const addEventAction = ( action, { kind, name, id, parentId, details,
|
|
71
|
+
export const addEventAction = ( action, { kind, name, id, parentId, details, traceInfo } ) => {
|
|
76
72
|
// Ignores internal steps in the actual trace files, ignore trace if the flag is true
|
|
77
|
-
if ( kind !== ComponentType.INTERNAL_STEP && !
|
|
73
|
+
if ( kind !== ComponentType.INTERNAL_STEP && !traceInfo.disableTrace ) {
|
|
78
74
|
traceBus.emit( 'entry', {
|
|
79
|
-
|
|
75
|
+
traceInfo,
|
|
80
76
|
entry: { kind, action, name, id, parentId, timestamp: Date.now(), details: serializeDetails( details ) }
|
|
81
77
|
} );
|
|
82
78
|
}
|
|
@@ -93,7 +89,7 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, exe
|
|
|
93
89
|
export function addEventActionWithContext( action, options ) {
|
|
94
90
|
const storeContent = Storage.load();
|
|
95
91
|
if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
|
|
96
|
-
const { parentId,
|
|
92
|
+
const { parentId, traceInfo, addAttribute } = storeContent;
|
|
97
93
|
if ( action === EventAction.ADD_ATTR ) {
|
|
98
94
|
const attribute = options.details;
|
|
99
95
|
if ( !( attribute instanceof BaseAttribute ) ) {
|
|
@@ -102,6 +98,6 @@ export function addEventActionWithContext( action, options ) {
|
|
|
102
98
|
addAttribute( options.details );
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
|
-
addEventAction( action, { ...options, parentId,
|
|
101
|
+
addEventAction( action, { ...options, parentId, traceInfo } );
|
|
106
102
|
}
|
|
107
103
|
};
|
|
@@ -34,6 +34,14 @@ async function loadTraceEngine() {
|
|
|
34
34
|
return import( './trace_engine.js' );
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const traceInfo = {
|
|
38
|
+
workflowId: 'w1',
|
|
39
|
+
runId: 'r1',
|
|
40
|
+
workflowType: 'WF',
|
|
41
|
+
startTime: 1,
|
|
42
|
+
disableTrace: false
|
|
43
|
+
};
|
|
44
|
+
|
|
37
45
|
describe( 'tracing/trace_engine', () => {
|
|
38
46
|
beforeEach( () => {
|
|
39
47
|
vi.clearAllMocks();
|
|
@@ -52,9 +60,8 @@ describe( 'tracing/trace_engine', () => {
|
|
|
52
60
|
expect( localInitMock ).toHaveBeenCalledTimes( 1 );
|
|
53
61
|
expect( s3InitMock ).not.toHaveBeenCalled();
|
|
54
62
|
|
|
55
|
-
const executionContext = { disableTrace: false };
|
|
56
63
|
addEventAction( 'start', {
|
|
57
|
-
kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true },
|
|
64
|
+
kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, traceInfo
|
|
58
65
|
} );
|
|
59
66
|
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
60
67
|
const payload = localExecMock.mock.calls[0][0];
|
|
@@ -62,7 +69,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
62
69
|
expect( payload.entry.kind ).toBe( 'step' );
|
|
63
70
|
expect( payload.entry.action ).toBe( 'start' );
|
|
64
71
|
expect( payload.entry.details ).toEqual( { ok: true } );
|
|
65
|
-
expect( payload.
|
|
72
|
+
expect( payload.traceInfo ).toBe( traceInfo );
|
|
66
73
|
} );
|
|
67
74
|
|
|
68
75
|
it( 'addEventAction() emits an entry consumed by processors', async () => {
|
|
@@ -72,7 +79,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
72
79
|
|
|
73
80
|
addEventAction( 'end', {
|
|
74
81
|
kind: 'workflow', name: 'W', id: '2', parentId: 'p2', details: 'done',
|
|
75
|
-
|
|
82
|
+
traceInfo
|
|
76
83
|
} );
|
|
77
84
|
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
78
85
|
const payload = localExecMock.mock.calls[0][0];
|
|
@@ -81,14 +88,14 @@ describe( 'tracing/trace_engine', () => {
|
|
|
81
88
|
expect( payload.entry.details ).toBe( 'done' );
|
|
82
89
|
} );
|
|
83
90
|
|
|
84
|
-
it( 'addEventAction() does not emit when
|
|
91
|
+
it( 'addEventAction() does not emit when traceInfo.disableTrace is true', async () => {
|
|
85
92
|
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
86
93
|
const { init, addEventAction } = await loadTraceEngine();
|
|
87
94
|
await init();
|
|
88
95
|
|
|
89
96
|
addEventAction( 'start', {
|
|
90
97
|
kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
|
|
91
|
-
|
|
98
|
+
traceInfo: { ...traceInfo, disableTrace: true }
|
|
92
99
|
} );
|
|
93
100
|
expect( localExecMock ).not.toHaveBeenCalled();
|
|
94
101
|
} );
|
|
@@ -100,7 +107,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
100
107
|
|
|
101
108
|
addEventAction( 'start', {
|
|
102
109
|
kind: 'internal_step', name: 'Internal', id: '1', parentId: 'p', details: {},
|
|
103
|
-
|
|
110
|
+
traceInfo
|
|
104
111
|
} );
|
|
105
112
|
expect( localExecMock ).not.toHaveBeenCalled();
|
|
106
113
|
} );
|
|
@@ -109,7 +116,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
109
116
|
process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
|
|
110
117
|
storageLoadMock.mockReturnValue( {
|
|
111
118
|
parentId: 'ctx-p',
|
|
112
|
-
|
|
119
|
+
traceInfo
|
|
113
120
|
} );
|
|
114
121
|
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
115
122
|
await init();
|
|
@@ -117,7 +124,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
117
124
|
addEventActionWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
|
|
118
125
|
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
119
126
|
const payload = localExecMock.mock.calls[0][0];
|
|
120
|
-
expect( payload.
|
|
127
|
+
expect( payload.traceInfo ).toBe( traceInfo );
|
|
121
128
|
expect( payload.entry.parentId ).toBe( 'ctx-p' );
|
|
122
129
|
expect( payload.entry.name ).toBe( 'S' );
|
|
123
130
|
expect( payload.entry.action ).toBe( 'tick' );
|
|
@@ -126,10 +133,9 @@ describe( 'tracing/trace_engine', () => {
|
|
|
126
133
|
it( 'addEventActionWithContext() records ADD_ATTR attributes through storage context', async () => {
|
|
127
134
|
process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
|
|
128
135
|
const addAttributeMock = vi.fn();
|
|
129
|
-
const executionContext = { runId: 'r1', disableTrace: false };
|
|
130
136
|
storageLoadMock.mockReturnValue( {
|
|
131
137
|
parentId: 'ctx-p',
|
|
132
|
-
|
|
138
|
+
traceInfo,
|
|
133
139
|
addAttribute: addAttributeMock
|
|
134
140
|
} );
|
|
135
141
|
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
@@ -144,7 +150,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
144
150
|
expect( addAttributeMock ).toHaveBeenCalledWith( attribute );
|
|
145
151
|
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
146
152
|
expect( localExecMock.mock.calls[0][0] ).toEqual( {
|
|
147
|
-
|
|
153
|
+
traceInfo,
|
|
148
154
|
entry: {
|
|
149
155
|
kind: 'http',
|
|
150
156
|
action: EventAction.ADD_ATTR,
|
|
@@ -162,7 +168,7 @@ describe( 'tracing/trace_engine', () => {
|
|
|
162
168
|
const addAttributeMock = vi.fn();
|
|
163
169
|
storageLoadMock.mockReturnValue( {
|
|
164
170
|
parentId: 'ctx-p',
|
|
165
|
-
|
|
171
|
+
traceInfo,
|
|
166
172
|
addAttribute: addAttributeMock
|
|
167
173
|
} );
|
|
168
174
|
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
@@ -179,11 +185,11 @@ describe( 'tracing/trace_engine', () => {
|
|
|
179
185
|
expect( localExecMock ).not.toHaveBeenCalled();
|
|
180
186
|
} );
|
|
181
187
|
|
|
182
|
-
it( 'addEventActionWithContext() does not emit when storage
|
|
188
|
+
it( 'addEventActionWithContext() does not emit when storage traceInfo.disableTrace is true', async () => {
|
|
183
189
|
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
184
190
|
storageLoadMock.mockReturnValue( {
|
|
185
191
|
parentId: 'ctx-p',
|
|
186
|
-
|
|
192
|
+
traceInfo: { ...traceInfo, disableTrace: true }
|
|
187
193
|
} );
|
|
188
194
|
const { init, addEventActionWithContext } = await loadTraceEngine();
|
|
189
195
|
await init();
|
|
@@ -203,21 +209,19 @@ describe( 'tracing/trace_engine', () => {
|
|
|
203
209
|
} );
|
|
204
210
|
|
|
205
211
|
describe( 'getDestinations()', () => {
|
|
206
|
-
const executionContext = { workflowId: 'w1', workflowName: 'WF', startTime: 1, disableTrace: false };
|
|
207
|
-
|
|
208
212
|
it( 'returns null for both when traces are off (env vars unset)', async () => {
|
|
209
213
|
const { getDestinations } = await loadTraceEngine();
|
|
210
|
-
const result = getDestinations(
|
|
214
|
+
const result = getDestinations( traceInfo );
|
|
211
215
|
expect( result ).toEqual( { local: null, remote: null } );
|
|
212
216
|
expect( localGetDestinationMock ).not.toHaveBeenCalled();
|
|
213
217
|
expect( s3GetDestinationMock ).not.toHaveBeenCalled();
|
|
214
218
|
} );
|
|
215
219
|
|
|
216
|
-
it( 'returns null for both when
|
|
220
|
+
it( 'returns null for both when traceInfo.disableTrace is true', async () => {
|
|
217
221
|
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
218
222
|
process.env.OUTPUT_TRACE_REMOTE_ON = '1';
|
|
219
223
|
const { getDestinations } = await loadTraceEngine();
|
|
220
|
-
const result = getDestinations( { ...
|
|
224
|
+
const result = getDestinations( { ...traceInfo, disableTrace: true } );
|
|
221
225
|
expect( result ).toEqual( { local: null, remote: null } );
|
|
222
226
|
expect( localGetDestinationMock ).not.toHaveBeenCalled();
|
|
223
227
|
expect( s3GetDestinationMock ).not.toHaveBeenCalled();
|
|
@@ -227,24 +231,24 @@ describe( 'tracing/trace_engine', () => {
|
|
|
227
231
|
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
228
232
|
process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
|
|
229
233
|
const { getDestinations } = await loadTraceEngine();
|
|
230
|
-
const result = getDestinations(
|
|
234
|
+
const result = getDestinations( traceInfo );
|
|
231
235
|
expect( result ).toEqual( {
|
|
232
236
|
local: '/local/path.json',
|
|
233
237
|
remote: 'https://bucket.s3.amazonaws.com/key.json'
|
|
234
238
|
} );
|
|
235
239
|
expect( localGetDestinationMock ).toHaveBeenCalledTimes( 1 );
|
|
236
|
-
expect( localGetDestinationMock ).toHaveBeenCalledWith(
|
|
240
|
+
expect( localGetDestinationMock ).toHaveBeenCalledWith( traceInfo );
|
|
237
241
|
expect( s3GetDestinationMock ).toHaveBeenCalledTimes( 1 );
|
|
238
|
-
expect( s3GetDestinationMock ).toHaveBeenCalledWith(
|
|
242
|
+
expect( s3GetDestinationMock ).toHaveBeenCalledWith( traceInfo );
|
|
239
243
|
} );
|
|
240
244
|
|
|
241
245
|
it( 'returns local only when local trace on and remote off', async () => {
|
|
242
246
|
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
243
247
|
process.env.OUTPUT_TRACE_REMOTE_ON = '0';
|
|
244
248
|
const { getDestinations } = await loadTraceEngine();
|
|
245
|
-
const result = getDestinations(
|
|
249
|
+
const result = getDestinations( traceInfo );
|
|
246
250
|
expect( result ).toEqual( { local: '/local/path.json', remote: null } );
|
|
247
|
-
expect( localGetDestinationMock ).toHaveBeenCalledWith(
|
|
251
|
+
expect( localGetDestinationMock ).toHaveBeenCalledWith( traceInfo );
|
|
248
252
|
expect( s3GetDestinationMock ).not.toHaveBeenCalled();
|
|
249
253
|
} );
|
|
250
254
|
|
|
@@ -252,10 +256,10 @@ describe( 'tracing/trace_engine', () => {
|
|
|
252
256
|
process.env.OUTPUT_TRACE_LOCAL_ON = '0';
|
|
253
257
|
process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
|
|
254
258
|
const { getDestinations } = await loadTraceEngine();
|
|
255
|
-
const result = getDestinations(
|
|
259
|
+
const result = getDestinations( traceInfo );
|
|
256
260
|
expect( result ).toEqual( { local: null, remote: 'https://bucket.s3.amazonaws.com/key.json' } );
|
|
257
261
|
expect( localGetDestinationMock ).not.toHaveBeenCalled();
|
|
258
|
-
expect( s3GetDestinationMock ).toHaveBeenCalledWith(
|
|
262
|
+
expect( s3GetDestinationMock ).toHaveBeenCalledWith( traceInfo );
|
|
259
263
|
} );
|
|
260
264
|
} );
|
|
261
265
|
} );
|
package/src/worker/configs.js
CHANGED
|
@@ -7,6 +7,7 @@ const coalesceEmptyString = v => v === '' ? undefined : v;
|
|
|
7
7
|
|
|
8
8
|
const envVarSchema = z.object( {
|
|
9
9
|
OUTPUT_CATALOG_ID: z.string().regex( /^[a-z0-9_.@-]+$/i ),
|
|
10
|
+
OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().nonnegative().default( 0 ) ),
|
|
10
11
|
TEMPORAL_ADDRESS: z.string().default( 'localhost:7233' ),
|
|
11
12
|
TEMPORAL_API_KEY: z.string().optional(),
|
|
12
13
|
TEMPORAL_NAMESPACE: z.string().optional().default( 'default' ),
|
|
@@ -51,6 +52,7 @@ export const maxConcurrentWorkflowTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_WO
|
|
|
51
52
|
export const namespace = envVars.TEMPORAL_NAMESPACE;
|
|
52
53
|
export const taskQueue = envVars.OUTPUT_CATALOG_ID;
|
|
53
54
|
export const catalogId = envVars.OUTPUT_CATALOG_ID;
|
|
55
|
+
export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVAL_MS;
|
|
54
56
|
export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
|
|
55
57
|
export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
|
|
56
58
|
export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
|
|
@@ -10,6 +10,7 @@ const CONFIG_KEYS = [
|
|
|
10
10
|
'TEMPORAL_MAX_CACHED_WORKFLOWS',
|
|
11
11
|
'TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS',
|
|
12
12
|
'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
|
|
13
|
+
'OUTPUT_WORKER_TELEMETRY_INTERVAL_MS',
|
|
13
14
|
'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
|
|
14
15
|
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED'
|
|
15
16
|
];
|
|
@@ -61,6 +62,7 @@ describe( 'worker/configs', () => {
|
|
|
61
62
|
expect( configs.maxCachedWorkflows ).toBe( 1000 );
|
|
62
63
|
expect( configs.maxConcurrentActivityTaskPolls ).toBe( 5 );
|
|
63
64
|
expect( configs.maxConcurrentWorkflowTaskPolls ).toBe( 5 );
|
|
65
|
+
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
64
66
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
|
|
65
67
|
expect( configs.activityHeartbeatEnabled ).toBe( true );
|
|
66
68
|
expect( configs.taskQueue ).toBe( 'test-catalog' );
|
|
@@ -74,11 +76,19 @@ describe( 'worker/configs', () => {
|
|
|
74
76
|
expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 40 );
|
|
75
77
|
} );
|
|
76
78
|
|
|
79
|
+
it( 'treats empty string for worker telemetry interval as default', async () => {
|
|
80
|
+
setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '' } );
|
|
81
|
+
const configs = await loadConfigs();
|
|
82
|
+
|
|
83
|
+
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
84
|
+
} );
|
|
85
|
+
|
|
77
86
|
it( 'parses custom numeric env vars', async () => {
|
|
78
87
|
setEnv( {
|
|
79
88
|
TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '10',
|
|
80
89
|
TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS: '50',
|
|
81
90
|
TEMPORAL_MAX_CACHED_WORKFLOWS: '500',
|
|
91
|
+
OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '30000',
|
|
82
92
|
OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: '60000'
|
|
83
93
|
} );
|
|
84
94
|
const configs = await loadConfigs();
|
|
@@ -86,9 +96,24 @@ describe( 'worker/configs', () => {
|
|
|
86
96
|
expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 10 );
|
|
87
97
|
expect( configs.maxConcurrentWorkflowTaskExecutions ).toBe( 50 );
|
|
88
98
|
expect( configs.maxCachedWorkflows ).toBe( 500 );
|
|
99
|
+
expect( configs.workerTelemetryIntervalMs ).toBe( 30000 );
|
|
89
100
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 60000 );
|
|
90
101
|
} );
|
|
91
102
|
|
|
103
|
+
it( 'allows zero for worker telemetry interval', async () => {
|
|
104
|
+
setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '0' } );
|
|
105
|
+
const configs = await loadConfigs();
|
|
106
|
+
|
|
107
|
+
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'throws when worker telemetry interval is negative', async () => {
|
|
111
|
+
setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '-1' } );
|
|
112
|
+
vi.resetModules();
|
|
113
|
+
|
|
114
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
115
|
+
} );
|
|
116
|
+
|
|
92
117
|
it( 'throws when optional number is zero or negative', async () => {
|
|
93
118
|
setEnv( { TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '0' } );
|
|
94
119
|
vi.resetModules();
|
package/src/worker/index.js
CHANGED
|
@@ -11,9 +11,10 @@ import { registerShutdown } from './shutdown.js';
|
|
|
11
11
|
import { startCatalog } from './start_catalog.js';
|
|
12
12
|
import { bootstrapFetchProxy } from './proxy.js';
|
|
13
13
|
import { messageBus } from '#bus';
|
|
14
|
-
import './log_hooks.js';
|
|
15
14
|
import { BusEventType } from '#consts';
|
|
16
15
|
import { hashSourceCode } from './loader_tools.js';
|
|
16
|
+
import { setupTelemetry } from './setup_telemetry.js';
|
|
17
|
+
import './log_hooks.js';
|
|
17
18
|
|
|
18
19
|
const log = createChildLogger( 'Worker' );
|
|
19
20
|
|
|
@@ -84,6 +85,8 @@ const callerDir = process.argv[2];
|
|
|
84
85
|
|
|
85
86
|
registerShutdown( { worker, log } );
|
|
86
87
|
|
|
88
|
+
setupTelemetry( { worker } );
|
|
89
|
+
|
|
87
90
|
log.info( 'Running worker...' );
|
|
88
91
|
await Promise.all( [ worker.run(), startCatalog( { connection, namespace, catalog, catalogHash } ) ] );
|
|
89
92
|
|
package/src/worker/index.spec.js
CHANGED
|
@@ -62,6 +62,9 @@ vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: bootstrapFetchProxyMock }
|
|
|
62
62
|
const registerShutdownMock = vi.fn();
|
|
63
63
|
vi.mock( './shutdown.js', () => ( { registerShutdown: registerShutdownMock } ) );
|
|
64
64
|
|
|
65
|
+
const setupTelemetryMock = vi.fn();
|
|
66
|
+
vi.mock( './setup_telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
|
|
67
|
+
|
|
65
68
|
vi.mock( './log_hooks.js', () => ( {} ) );
|
|
66
69
|
|
|
67
70
|
const runState = { resolve: null };
|
|
@@ -129,6 +132,7 @@ describe( 'worker/index', () => {
|
|
|
129
132
|
} ) );
|
|
130
133
|
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [], connection: mockConnection } );
|
|
131
134
|
expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
|
|
135
|
+
expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
|
|
132
136
|
expect( startCatalogMock ).toHaveBeenCalledWith( {
|
|
133
137
|
connection: mockConnection,
|
|
134
138
|
namespace: configValues.namespace,
|