@outputai/core 0.2.1-next.380dff4.0 → 0.2.1-next.52e960c.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 +1 -1
- package/src/activity_integration/tracing.d.ts +30 -17
- package/src/activity_integration/tracing.js +33 -17
- package/src/tracing/internal_interface.js +34 -28
- package/src/tracing/processors/local/index.js +43 -10
- package/src/tracing/processors/local/index.spec.js +66 -36
- package/src/tracing/processors/s3/index.js +10 -5
- package/src/tracing/processors/s3/index.spec.js +7 -7
- package/src/tracing/tools/build_trace_tree.js +17 -12
- package/src/tracing/tools/build_trace_tree.spec.js +63 -20
- package/src/tracing/tools/utils.js +28 -0
- package/src/tracing/tools/utils.spec.js +134 -2
- package/src/tracing/trace_consts.js +9 -0
- package/src/tracing/trace_engine.js +10 -10
- package/src/tracing/trace_engine.spec.js +23 -23
- package/src/worker/configs.js +9 -1
- package/src/worker/index.js +8 -2
- package/src/worker/index.spec.js +7 -1
- package/src/worker/proxy.js +32 -0
- package/src/worker/proxy.spec.js +71 -0
package/package.json
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Creates a new event.
|
|
3
3
|
*
|
|
4
|
-
* @param args
|
|
5
|
-
* @param args.id - A unique id for the Event
|
|
4
|
+
* @param args
|
|
5
|
+
* @param args.id - A unique id for the Event.
|
|
6
6
|
* @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
7
|
-
* @param args.name - The human
|
|
8
|
-
* @param args.details - Arbitrary
|
|
7
|
+
* @param args.name - The human-friendly name of the Event: query, request, create.
|
|
8
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
9
9
|
*/
|
|
10
10
|
export declare function addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Concludes an event.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @param args -
|
|
18
|
-
* @param args.id - Identifier matching the event's start phase.
|
|
19
|
-
* @param args.details - Arbitrary metadata associated with this phase (e.g., results, response body).
|
|
15
|
+
* @param args
|
|
16
|
+
* @param args.id - The id of the event to conclude.
|
|
17
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
|
|
20
18
|
*/
|
|
21
19
|
export declare function addEventEnd( args: { id: string; details: unknown } ): void;
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Use the same id as the start phase to correlate phases.
|
|
22
|
+
* Concludes an event with an error.
|
|
27
23
|
*
|
|
28
|
-
* @param args
|
|
29
|
-
* @param args.id -
|
|
30
|
-
* @param args.details - Arbitrary
|
|
24
|
+
* @param args
|
|
25
|
+
* @param args.id - The id of the event to conclude.
|
|
26
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
|
|
31
27
|
*/
|
|
32
28
|
export declare function addEventError( args: { id: string; details: unknown } ): void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Adds an attribute to an event.
|
|
32
|
+
*
|
|
33
|
+
* @param args
|
|
34
|
+
* @param args.eventId - The id of the event to attach the attribute to.
|
|
35
|
+
* @param args.name - The attribute name
|
|
36
|
+
* @param args.value - The attribute value
|
|
37
|
+
*/
|
|
38
|
+
export declare function addEventAttribute( args: { eventId: string; name: string, value: unknown } ): void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Known attributes.
|
|
42
|
+
*/
|
|
43
|
+
export declare const Attribute: {
|
|
44
|
+
COST: 'cost';
|
|
45
|
+
};
|
|
@@ -1,37 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addEventActionWithContext, EventAction } from '#tracing';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Creates a new event.
|
|
5
5
|
*
|
|
6
6
|
* @param {object} args
|
|
7
|
-
* @param {string} args.id - A unique id for the Event
|
|
7
|
+
* @param {string} args.id - A unique id for the Event.
|
|
8
8
|
* @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
9
|
-
* @param {string} args.name - The human
|
|
10
|
-
* @param {object} args.details -
|
|
9
|
+
* @param {string} args.name - The human-friendly name of the Event: query, request, create.
|
|
10
|
+
* @param {object} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
11
11
|
* @returns {void}
|
|
12
12
|
*/
|
|
13
|
-
export const addEventStart = ( { id, kind, name, details } ) =>
|
|
13
|
+
export const addEventStart = ( { id, kind, name, details } ) =>
|
|
14
|
+
addEventActionWithContext( EventAction.START, { kind, name, details, id } );
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* It needs to use the same id of the start phase.
|
|
17
|
+
* Concludes an event.
|
|
19
18
|
*
|
|
20
19
|
* @param {object} args
|
|
21
|
-
* @param {string} args.id -
|
|
22
|
-
* @param {object} args.details -
|
|
20
|
+
* @param {string} args.id - The id of the event to conclude.
|
|
21
|
+
* @param {object} args.details - Arbitrary data to add to this event, it will be used as the "output" field.
|
|
23
22
|
* @returns {void}
|
|
24
23
|
*/
|
|
25
|
-
export const addEventEnd = ( { id, details } ) =>
|
|
24
|
+
export const addEventEnd = ( { id, details } ) => addEventActionWithContext( EventAction.END, { id, details } );
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
27
|
+
* Concludes an event with an error.
|
|
29
28
|
*
|
|
30
|
-
*
|
|
29
|
+
* @param {object} args
|
|
30
|
+
* @param {string} args.id - The id of the event to conclude.
|
|
31
|
+
* @param {object} args.details - Arbitrary data to add to this event, it will be used as the "error" field.
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
export const addEventError = ( { id, details } ) => addEventActionWithContext( EventAction.ERROR, { id, details } );
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adds an attribute to an event.
|
|
31
38
|
*
|
|
32
39
|
* @param {object} args
|
|
33
|
-
* @param {string} args.
|
|
34
|
-
* @param {
|
|
40
|
+
* @param {string} args.eventId - The id of the event to attach the attribute to.
|
|
41
|
+
* @param {string} args.name - The attribute name
|
|
42
|
+
* @param {unknown} args.value - The attribute value
|
|
35
43
|
* @returns {void}
|
|
36
44
|
*/
|
|
37
|
-
export const
|
|
45
|
+
export const addEventAttribute = ( { eventId, name, value } ) =>
|
|
46
|
+
addEventActionWithContext( EventAction.ADD_ATTR, { id: eventId, details: { name, value } } );
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Known attributes
|
|
50
|
+
*/
|
|
51
|
+
export const Attribute = {
|
|
52
|
+
COST: 'cost'
|
|
53
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addEventAction, addEventActionWithContext, init, getDestinations } from './trace_engine.js';
|
|
2
|
+
import { EventAction } from './trace_consts.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Init method, if not called, no processors are attached and trace functions are dummy
|
|
@@ -6,66 +7,71 @@ import { addEventPhase, addEventPhaseWithContext, init, getDestinations } from '
|
|
|
6
7
|
export { init, getDestinations };
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Internal use only - adds
|
|
10
|
+
* Internal use only - adds an action with AsyncLocalStorage context resolution
|
|
10
11
|
*/
|
|
11
|
-
export {
|
|
12
|
+
export { addEventActionWithContext };
|
|
13
|
+
|
|
14
|
+
export { EventAction };
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Trace nomenclature
|
|
15
18
|
*
|
|
16
19
|
* Trace - The collection of Events;
|
|
17
|
-
* Event - Any entry in the Trace file
|
|
18
|
-
*
|
|
20
|
+
* Event - Any entry in the Trace file must have both START and END/ERROR actions, plus any number of ADD_ATTR actions;
|
|
21
|
+
* Action - A specific part of an Event: START, END, ERROR, or ADD_ATTR;
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Internal use only
|
|
23
26
|
*
|
|
24
|
-
*
|
|
27
|
+
* Creates a new event and appends it to the trace.
|
|
25
28
|
*
|
|
26
29
|
* @param {object} args
|
|
27
|
-
* @param {string} args.id - A unique id for the Event
|
|
30
|
+
* @param {string} args.id - A unique id for the Event.
|
|
28
31
|
* @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
29
|
-
* @param {string} args.name - The human
|
|
30
|
-
* @param {any} args.details -
|
|
31
|
-
* @param {string} args.parentId - The parent Event, used to build a
|
|
32
|
+
* @param {string} args.name - The human-friendly name of the Event: query, request, create.
|
|
33
|
+
* @param {any} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
34
|
+
* @param {string} args.parentId - The parent Event, used to build a tree.
|
|
32
35
|
* @param {object} args.executionContext - The original execution context from the workflow
|
|
33
36
|
* @returns {void}
|
|
34
37
|
*/
|
|
35
|
-
export const addEventStart = options =>
|
|
38
|
+
export const addEventStart = options => addEventAction( EventAction.START, options );
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Internal use only
|
|
39
42
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* It needs to use the same id of the start phase.
|
|
43
|
+
* Concludes an event, matching by its id.
|
|
43
44
|
*
|
|
44
45
|
* @param {object} args
|
|
45
|
-
* @param {string} args.id -
|
|
46
|
-
* @param {
|
|
47
|
-
* @param {string} args.name - The human friendly name of the Event: query, request, create.
|
|
48
|
-
* @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
|
|
49
|
-
* @param {string} args.parentId - The parent Event, used to build a three.
|
|
46
|
+
* @param {string} args.id - The id of the event to conclude.
|
|
47
|
+
* @param {any} args.details - Arbitrary data to add to the event; it is used as the "output" field.
|
|
50
48
|
* @param {object} args.executionContext - The original execution context from the workflow
|
|
51
49
|
* @returns {void}
|
|
52
50
|
*/
|
|
53
|
-
export const addEventEnd = options =>
|
|
51
|
+
export const addEventEnd = options => addEventAction( EventAction.END, options );
|
|
54
52
|
|
|
55
53
|
/**
|
|
56
54
|
* Internal use only
|
|
57
55
|
*
|
|
58
|
-
*
|
|
56
|
+
* Concludes an event with an error, matching by its id.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} args
|
|
59
|
+
* @param {string} args.id - The id of the event to conclude.
|
|
60
|
+
* @param {any} args.details - Arbitrary data to add to the event; it is used as the "error" field.
|
|
61
|
+
* @param {object} args.executionContext - The original execution context from the workflow
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
export const addEventError = options => addEventAction( EventAction.ERROR, options );
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Internal use only
|
|
59
68
|
*
|
|
60
|
-
*
|
|
69
|
+
* Adds an attribute to an event using its id.
|
|
61
70
|
*
|
|
62
71
|
* @param {object} args
|
|
63
|
-
* @param {string} args.id -
|
|
64
|
-
* @param {
|
|
65
|
-
* @param {string} args.name - The human friendly name of the Event: query, request, create.
|
|
66
|
-
* @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
|
|
67
|
-
* @param {string} args.parentId - The parent Event, used to build a three.
|
|
72
|
+
* @param {string} args.id - The id of the event to attach the attribute to.
|
|
73
|
+
* @param {object} args.details - The attribute to add to this event, must be in `{ name: string, value: any }` format.
|
|
68
74
|
* @param {object} args.executionContext - The original execution context from the workflow
|
|
69
75
|
* @returns {void}
|
|
70
76
|
*/
|
|
71
|
-
export const
|
|
77
|
+
export const addEventAttribute = options => addEventAction( EventAction.ADD_ATTR, options );
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFile
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import buildTraceTree from '../../tools/build_trace_tree.js';
|
|
5
|
+
import { safeFormatJSON } from '../../tools/utils.js';
|
|
5
6
|
import { EOL } from 'node:os';
|
|
6
7
|
|
|
7
8
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
@@ -13,12 +14,32 @@ const callerDir = process.argv[2];
|
|
|
13
14
|
|
|
14
15
|
const tempTraceFilesDir = join( __dirname, 'temp', 'traces' );
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds the temp file path to accumulate trace entries
|
|
19
|
+
*
|
|
20
|
+
* @param {object} executionContext - The execution context around a given trace entry
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
const createTempFilePath = ( { workflowId, startTime } ) => join( tempTraceFilesDir, `${startTime}_${workflowId}.trace` );
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Adds a trace entry to the accumulation file.
|
|
27
|
+
* @param {object} entry - The trace entry
|
|
28
|
+
* @param {string} path - Accumulation file path
|
|
29
|
+
*/
|
|
30
|
+
const addEntry = ( entry, path ) => appendFileSync( path, JSON.stringify( entry ) + EOL, 'utf-8' );
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads the accumulation file and returns all the entries, each serialized to JSON
|
|
34
|
+
* @param {string} path - Accumulation file path
|
|
35
|
+
* @returns {object[]} Trace entries
|
|
36
|
+
*/
|
|
37
|
+
const getEntries = path => readFileSync( path, 'utf-8' ).split( EOL ).slice( 0, -1 ).map( v => JSON.parse( v ) );
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Deletes old accumulation files
|
|
41
|
+
* @param {number} [threshold] Timestamp in ms epoch. All files below this date are considered old
|
|
42
|
+
*/
|
|
22
43
|
const cleanupOldTempFiles = ( threshold = Date.now() - tempFilesTTL ) =>
|
|
23
44
|
readdirSync( tempTraceFilesDir )
|
|
24
45
|
.filter( f => +f.split( '_' )[0] < threshold )
|
|
@@ -79,21 +100,33 @@ export const init = () => {
|
|
|
79
100
|
/**
|
|
80
101
|
* Execute this processor:
|
|
81
102
|
*
|
|
82
|
-
*
|
|
103
|
+
* Appends each trace entry to a temp file.
|
|
104
|
+
*
|
|
105
|
+
* When the root workflow ends or the entry is an error action, build the trace tree and write the JSON file.
|
|
83
106
|
*
|
|
84
107
|
* @param {object} args
|
|
85
|
-
* @param {object} entry -
|
|
86
|
-
* @param {object} executionContext - Execution info: workflowId, workflowName, startTime
|
|
108
|
+
* @param {object} args.entry - The trace entry to append.
|
|
109
|
+
* @param {object} args.executionContext - Execution info: workflowId, workflowName, startTime
|
|
87
110
|
* @returns {void}
|
|
88
111
|
*/
|
|
89
112
|
export const exec = ( { entry, executionContext } ) => {
|
|
90
113
|
const { workflowId, workflowName, startTime } = executionContext;
|
|
91
|
-
const
|
|
114
|
+
const tempFilePath = createTempFilePath( executionContext );
|
|
115
|
+
addEntry( entry, tempFilePath );
|
|
116
|
+
|
|
117
|
+
const isRootWorkflowEnd = entry.id === workflowId && entry.action !== 'start';
|
|
118
|
+
const isError = entry.action === 'error';
|
|
119
|
+
|
|
120
|
+
if ( !isRootWorkflowEnd && !isError ) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const content = buildTraceTree( getEntries( tempFilePath ) );
|
|
92
125
|
const dir = resolveIOPath( workflowName );
|
|
93
126
|
const path = join( dir, buildTraceFilename( { startTime, workflowId } ) );
|
|
94
127
|
|
|
95
128
|
mkdirSync( dir, { recursive: true } );
|
|
96
|
-
writeFileSync( path,
|
|
129
|
+
writeFileSync( path, safeFormatJSON( content ) + EOL, 'utf-8' );
|
|
97
130
|
};
|
|
98
131
|
|
|
99
132
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { EOL } from 'node:os';
|
|
2
3
|
|
|
3
4
|
// In-memory fs mock store
|
|
4
5
|
const store = { files: new Map() };
|
|
@@ -24,12 +25,17 @@ vi.mock( 'node:fs', () => ( {
|
|
|
24
25
|
const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
|
|
25
26
|
vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
|
|
26
27
|
|
|
28
|
+
/** Flush happens when the root id matches workflowId and action is not 'start', or when action is 'error'. */
|
|
29
|
+
const rootStart = ( workflowId, ts ) => ( { id: workflowId, action: 'start', timestamp: ts } );
|
|
30
|
+
const rootEnd = ( workflowId, ts ) => ( { id: workflowId, action: 'end', timestamp: ts } );
|
|
31
|
+
const childTick = ( id, ts ) => ( { id, action: 'tick', timestamp: ts } );
|
|
32
|
+
|
|
27
33
|
describe( 'tracing/processors/local', () => {
|
|
28
34
|
beforeEach( () => {
|
|
29
35
|
vi.clearAllMocks();
|
|
30
36
|
store.files.clear();
|
|
31
37
|
process.argv[2] = '/tmp/project';
|
|
32
|
-
delete process.env.OUTPUT_TRACE_HOST_PATH;
|
|
38
|
+
delete process.env.OUTPUT_TRACE_HOST_PATH;
|
|
33
39
|
} );
|
|
34
40
|
|
|
35
41
|
it( 'init(): creates temp dir and cleans up old files', async () => {
|
|
@@ -40,34 +46,63 @@ describe( 'tracing/processors/local', () => {
|
|
|
40
46
|
|
|
41
47
|
init();
|
|
42
48
|
|
|
43
|
-
// Should create temp dir relative to module location using __dirname
|
|
44
49
|
expect( mkdirSyncMock ).toHaveBeenCalledWith( expect.stringMatching( /temp\/traces$/ ), { recursive: true } );
|
|
45
50
|
expect( rmSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
46
51
|
} );
|
|
47
52
|
|
|
48
|
-
it( 'exec():
|
|
53
|
+
it( 'exec(): appends each entry and writes aggregated tree once on root workflow end', async () => {
|
|
49
54
|
const { exec, init } = await import( './index.js' );
|
|
50
55
|
init();
|
|
51
56
|
|
|
52
57
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
53
|
-
const
|
|
58
|
+
const workflowId = 'id1';
|
|
59
|
+
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
54
60
|
|
|
55
|
-
exec( { ...ctx, entry:
|
|
56
|
-
exec( { ...ctx, entry:
|
|
57
|
-
exec( { ...ctx, entry:
|
|
61
|
+
exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
62
|
+
exec( { ...ctx, entry: childTick( 'child-1', startTime + 1 ) } );
|
|
63
|
+
exec( { ...ctx, entry: rootEnd( workflowId, startTime + 2 ) } );
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
expect( buildTraceTreeMock ).
|
|
61
|
-
expect( buildTraceTreeMock.mock.calls.at( -1 )[0].length ).toBe( 3 );
|
|
65
|
+
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
66
|
+
expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 3 );
|
|
62
67
|
|
|
63
|
-
expect( writeFileSyncMock ).toHaveBeenCalledTimes(
|
|
64
|
-
const [ writtenPath, content ] = writeFileSyncMock.mock.calls
|
|
65
|
-
|
|
66
|
-
expect( writtenPath ).toMatch( /\/runs\/WF\// );
|
|
68
|
+
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
69
|
+
const [ writtenPath, content ] = writeFileSyncMock.mock.calls[0];
|
|
70
|
+
expect( writtenPath ).toMatch( /\/tmp\/project\/logs\/runs\/WF\// );
|
|
67
71
|
expect( JSON.parse( content.trim() ).count ).toBe( 3 );
|
|
68
72
|
} );
|
|
69
73
|
|
|
70
|
-
it( '
|
|
74
|
+
it( 'exec(): does not build or write on non-flush entries', async () => {
|
|
75
|
+
const { exec, init } = await import( './index.js' );
|
|
76
|
+
init();
|
|
77
|
+
|
|
78
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
79
|
+
const workflowId = 'id1';
|
|
80
|
+
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
81
|
+
|
|
82
|
+
exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
83
|
+
exec( { ...ctx, entry: childTick( 'child-1', startTime + 1 ) } );
|
|
84
|
+
|
|
85
|
+
expect( buildTraceTreeMock ).not.toHaveBeenCalled();
|
|
86
|
+
expect( writeFileSyncMock ).not.toHaveBeenCalled();
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
it( 'exec(): flushes on error action before root end', async () => {
|
|
90
|
+
const { exec, init } = await import( './index.js' );
|
|
91
|
+
init();
|
|
92
|
+
|
|
93
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
94
|
+
const workflowId = 'id1';
|
|
95
|
+
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
96
|
+
|
|
97
|
+
exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
98
|
+
exec( { ...ctx, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
|
|
99
|
+
|
|
100
|
+
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
101
|
+
expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 2 );
|
|
102
|
+
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it( 'getDestination(): returns absolute path under callerDir logs', async () => {
|
|
71
106
|
const { getDestination } = await import( './index.js' );
|
|
72
107
|
|
|
73
108
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
@@ -76,36 +111,36 @@ describe( 'tracing/processors/local', () => {
|
|
|
76
111
|
|
|
77
112
|
const destination = getDestination( { startTime, workflowId, workflowName } );
|
|
78
113
|
|
|
79
|
-
|
|
80
|
-
expect( destination ).
|
|
81
|
-
|
|
114
|
+
expect( destination ).toMatch( /^\/|^[A-Z]:\\/i );
|
|
115
|
+
expect( destination ).toBe(
|
|
116
|
+
'/tmp/project/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json'
|
|
117
|
+
);
|
|
82
118
|
} );
|
|
83
119
|
|
|
84
|
-
it( 'exec(): writes
|
|
120
|
+
it( 'exec(): writes under process.argv[2] logs even when OUTPUT_TRACE_HOST_PATH is set', async () => {
|
|
85
121
|
const { exec, init } = await import( './index.js' );
|
|
86
122
|
|
|
87
|
-
// Set OUTPUT_TRACE_HOST_PATH to simulate Docker environment
|
|
88
123
|
process.env.OUTPUT_TRACE_HOST_PATH = '/host/path/logs';
|
|
89
124
|
|
|
90
125
|
init();
|
|
91
126
|
|
|
92
127
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
93
|
-
const
|
|
128
|
+
const workflowId = 'id1';
|
|
129
|
+
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
94
130
|
|
|
95
|
-
exec( { ...ctx, entry:
|
|
131
|
+
exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
132
|
+
exec( { ...ctx, entry: rootEnd( workflowId, startTime + 1 ) } );
|
|
96
133
|
|
|
97
134
|
expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
|
|
98
|
-
const [ writtenPath ] = writeFileSyncMock.mock.calls
|
|
135
|
+
const [ writtenPath ] = writeFileSyncMock.mock.calls[0];
|
|
99
136
|
|
|
100
|
-
// Should write to process.cwd()/logs, NOT to OUTPUT_TRACE_HOST_PATH
|
|
101
137
|
expect( writtenPath ).not.toContain( '/host/path/logs' );
|
|
102
|
-
expect( writtenPath ).toMatch(
|
|
138
|
+
expect( writtenPath ).toMatch( /\/tmp\/project\/logs\/runs\/WF\// );
|
|
103
139
|
} );
|
|
104
140
|
|
|
105
141
|
it( 'getDestination(): returns OUTPUT_TRACE_HOST_PATH when set', async () => {
|
|
106
142
|
const { getDestination } = await import( './index.js' );
|
|
107
143
|
|
|
108
|
-
// Set OUTPUT_TRACE_HOST_PATH to simulate Docker environment
|
|
109
144
|
process.env.OUTPUT_TRACE_HOST_PATH = '/host/path/logs';
|
|
110
145
|
|
|
111
146
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
@@ -114,14 +149,12 @@ describe( 'tracing/processors/local', () => {
|
|
|
114
149
|
|
|
115
150
|
const destination = getDestination( { startTime, workflowId, workflowName } );
|
|
116
151
|
|
|
117
|
-
// Should return OUTPUT_TRACE_HOST_PATH-based path for reporting
|
|
118
152
|
expect( destination ).toBe( '/host/path/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
|
|
119
153
|
} );
|
|
120
154
|
|
|
121
155
|
it( 'separation of write and report paths works correctly', async () => {
|
|
122
156
|
const { exec, getDestination, init } = await import( './index.js' );
|
|
123
157
|
|
|
124
|
-
// Set OUTPUT_TRACE_HOST_PATH to simulate Docker environment
|
|
125
158
|
process.env.OUTPUT_TRACE_HOST_PATH = '/Users/ben/project/logs';
|
|
126
159
|
|
|
127
160
|
init();
|
|
@@ -131,19 +164,16 @@ describe( 'tracing/processors/local', () => {
|
|
|
131
164
|
const workflowName = 'test-workflow';
|
|
132
165
|
const ctx = { executionContext: { workflowId, workflowName, startTime } };
|
|
133
166
|
|
|
134
|
-
|
|
135
|
-
exec( { ...ctx, entry:
|
|
167
|
+
exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
168
|
+
exec( { ...ctx, entry: rootEnd( workflowId, startTime + 1 ) } );
|
|
136
169
|
|
|
137
|
-
// Get destination for reporting
|
|
138
170
|
const destination = getDestination( { startTime, workflowId, workflowName } );
|
|
139
171
|
|
|
140
|
-
|
|
141
|
-
const [ writtenPath ] = writeFileSyncMock.mock.calls.at( -1 );
|
|
172
|
+
const [ writtenPath, payload ] = writeFileSyncMock.mock.calls[0];
|
|
142
173
|
expect( writtenPath ).not.toContain( '/Users/ben/project' );
|
|
143
|
-
expect( writtenPath ).toMatch(
|
|
174
|
+
expect( writtenPath ).toMatch( /\/tmp\/project\/logs\/runs\/test-workflow\// );
|
|
175
|
+
expect( payload.endsWith( EOL ) ).toBe( true );
|
|
144
176
|
|
|
145
|
-
// Verify report path uses OUTPUT_TRACE_HOST_PATH
|
|
146
177
|
expect( destination ).toBe( '/Users/ben/project/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
|
|
147
178
|
} );
|
|
148
179
|
} );
|
|
149
|
-
|
|
@@ -4,6 +4,7 @@ import buildTraceTree from '../../tools/build_trace_tree.js';
|
|
|
4
4
|
import { EOL } from 'node:os';
|
|
5
5
|
import { loadEnv, getVars } from './configs.js';
|
|
6
6
|
import { createChildLogger } from '#logger';
|
|
7
|
+
import { safeFormatJSON } from '../../tools/utils.js';
|
|
7
8
|
|
|
8
9
|
const log = createChildLogger( 'S3 Processor' );
|
|
9
10
|
|
|
@@ -66,11 +67,15 @@ export const init = async () => {
|
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
|
-
* Execute this processor:
|
|
70
|
+
* Execute this processor:
|
|
71
|
+
*
|
|
72
|
+
* Appends each trace entry to Redis.
|
|
73
|
+
*
|
|
74
|
+
* When the root workflow ends or the entry is an error action, it builds the trace tree and uploads it to S3.
|
|
70
75
|
*
|
|
71
76
|
* @param {object} args
|
|
72
|
-
* @param {object} entry -
|
|
73
|
-
* @param {object} executionContext - Execution info: workflowId, workflowName, startTime
|
|
77
|
+
* @param {object} args.entry - The trace entry to append
|
|
78
|
+
* @param {object} args.executionContext - Execution info: workflowId, workflowName, startTime
|
|
74
79
|
*/
|
|
75
80
|
export const exec = async ( { entry, executionContext } ) => {
|
|
76
81
|
const { workflowName, workflowId, startTime } = executionContext;
|
|
@@ -78,7 +83,7 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
78
83
|
|
|
79
84
|
await addEntry( entry, cacheKey );
|
|
80
85
|
|
|
81
|
-
const isRootWorkflowEnd = entry.id === workflowId && entry.
|
|
86
|
+
const isRootWorkflowEnd = entry.id === workflowId && entry.action !== 'start';
|
|
82
87
|
if ( !isRootWorkflowEnd ) {
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
@@ -97,7 +102,7 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
97
102
|
}
|
|
98
103
|
await upload( {
|
|
99
104
|
key: getS3Key( { workflowId, workflowName, startTime } ),
|
|
100
|
-
content:
|
|
105
|
+
content: safeFormatJSON( content ) + EOL
|
|
101
106
|
} );
|
|
102
107
|
await bustEntries( cacheKey );
|
|
103
108
|
};
|
|
@@ -52,9 +52,9 @@ describe( 'tracing/processors/s3', () => {
|
|
|
52
52
|
|
|
53
53
|
redisMulti.exec.mockResolvedValue( [] );
|
|
54
54
|
|
|
55
|
-
const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow',
|
|
56
|
-
const activityStart = { id: 'act-1', name: 'DoSomething', kind: 'step', parentId: 'id1',
|
|
57
|
-
const workflowEnd = { id: 'id1',
|
|
55
|
+
const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
|
|
56
|
+
const activityStart = { id: 'act-1', name: 'DoSomething', kind: 'step', parentId: 'id1', action: 'start', details: {}, timestamp: startTime + 1 };
|
|
57
|
+
const workflowEnd = { id: 'id1', action: 'end', details: { ok: true }, timestamp: startTime + 2 };
|
|
58
58
|
zRangeMock.mockResolvedValue( [
|
|
59
59
|
JSON.stringify( workflowStart ),
|
|
60
60
|
JSON.stringify( activityStart ),
|
|
@@ -97,7 +97,7 @@ describe( 'tracing/processors/s3', () => {
|
|
|
97
97
|
|
|
98
98
|
redisMulti.exec.mockResolvedValue( [] );
|
|
99
99
|
const workflowStart = {
|
|
100
|
-
kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined,
|
|
100
|
+
kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, action: 'start', details: {}, timestamp: startTime
|
|
101
101
|
};
|
|
102
102
|
zRangeMock.mockResolvedValue( [ JSON.stringify( workflowStart ) ] );
|
|
103
103
|
|
|
@@ -113,8 +113,8 @@ describe( 'tracing/processors/s3', () => {
|
|
|
113
113
|
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
114
114
|
|
|
115
115
|
redisMulti.exec.mockResolvedValue( [] );
|
|
116
|
-
const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow',
|
|
117
|
-
const stepEndNoParent = { id: 'step-1',
|
|
116
|
+
const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
|
|
117
|
+
const stepEndNoParent = { id: 'step-1', action: 'end', details: { done: true }, timestamp: startTime + 1 };
|
|
118
118
|
zRangeMock.mockResolvedValue( [
|
|
119
119
|
JSON.stringify( workflowStart ),
|
|
120
120
|
JSON.stringify( stepEndNoParent )
|
|
@@ -136,7 +136,7 @@ describe( 'tracing/processors/s3', () => {
|
|
|
136
136
|
|
|
137
137
|
redisMulti.exec.mockResolvedValue( [] );
|
|
138
138
|
const workflowEnd = {
|
|
139
|
-
kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined,
|
|
139
|
+
kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, action: 'end', details: {}, timestamp: startTime
|
|
140
140
|
};
|
|
141
141
|
zRangeMock.mockResolvedValue( [ JSON.stringify( workflowEnd ) ] );
|
|
142
142
|
buildTraceTreeMock.mockReturnValueOnce( null );
|