@output.ai/core 0.4.5 → 0.4.7
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/consts.js +10 -0
- package/src/index.js +1 -1
- package/src/internal_activities/index.spec.js +10 -7
- package/src/logger.js +19 -2
- package/src/tracing/processors/s3/index.js +1 -1
- package/src/tracing/processors/s3/index.spec.js +14 -0
- package/src/utils/index.d.ts +8 -0
- package/src/utils/utils.js +11 -0
- package/src/worker/interceptors/activity.js +11 -2
- package/src/worker/interceptors/activity.spec.js +9 -6
- package/src/worker/sinks.js +29 -42
- package/src/logger.d.ts +0 -4
package/package.json
CHANGED
package/src/consts.js
CHANGED
|
@@ -10,3 +10,13 @@ export const ComponentType = {
|
|
|
10
10
|
INTERNAL_STEP: 'internal_step',
|
|
11
11
|
STEP: 'step'
|
|
12
12
|
};
|
|
13
|
+
export const LifecycleEvent = {
|
|
14
|
+
START: 'start',
|
|
15
|
+
END: 'end',
|
|
16
|
+
ERROR: 'error'
|
|
17
|
+
};
|
|
18
|
+
export const LifecycleEventLogMessage = {
|
|
19
|
+
[LifecycleEvent.START]: 'Start',
|
|
20
|
+
[LifecycleEvent.END]: 'End',
|
|
21
|
+
[LifecycleEvent.ERROR]: 'Error'
|
|
22
|
+
};
|
package/src/index.js
CHANGED
|
@@ -4,15 +4,18 @@ import { FatalError } from '#errors';
|
|
|
4
4
|
import { serializeBodyAndInferContentType, serializeFetchResponse } from '#utils';
|
|
5
5
|
import { sendHttpRequest } from './index.js';
|
|
6
6
|
|
|
7
|
-
vi.mock( '#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
isStringboolTrue: vi.fn( () => false ),
|
|
11
|
-
serializeBodyAndInferContentType: vi.fn(),
|
|
12
|
-
serializeFetchResponse: vi.fn()
|
|
13
|
-
};
|
|
7
|
+
vi.mock( '#logger', () => {
|
|
8
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
9
|
+
return { createChildLogger: vi.fn( () => log ) };
|
|
14
10
|
} );
|
|
15
11
|
|
|
12
|
+
vi.mock( '#utils', () => ( {
|
|
13
|
+
setMetadata: vi.fn(),
|
|
14
|
+
isStringboolTrue: vi.fn( () => false ),
|
|
15
|
+
serializeBodyAndInferContentType: vi.fn(),
|
|
16
|
+
serializeFetchResponse: vi.fn()
|
|
17
|
+
} ) );
|
|
18
|
+
|
|
16
19
|
const mockAgent = new MockAgent();
|
|
17
20
|
mockAgent.disableNetConnect();
|
|
18
21
|
|
package/src/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import winston from 'winston';
|
|
2
|
+
import { shuffleArray } from '#utils';
|
|
2
3
|
|
|
3
4
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
4
5
|
|
|
@@ -10,6 +11,16 @@ const levels = {
|
|
|
10
11
|
debug: 4
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
const colors = shuffleArray( [
|
|
15
|
+
'033', // blue
|
|
16
|
+
'030', // green
|
|
17
|
+
'208', // orange
|
|
18
|
+
'045', // turquoise
|
|
19
|
+
'129', // purple
|
|
20
|
+
'184' // yellow
|
|
21
|
+
] );
|
|
22
|
+
const assignedColors = new Map();
|
|
23
|
+
|
|
13
24
|
// Format metadata as friendly JSON: "{ name: "foo", count: 5 }"
|
|
14
25
|
const formatMeta = obj => {
|
|
15
26
|
const entries = Object.entries( obj );
|
|
@@ -18,14 +29,20 @@ const formatMeta = obj => {
|
|
|
18
29
|
}
|
|
19
30
|
return ' { ' + entries.map( ( [ k, v ] ) => `${k}: ${JSON.stringify( v )}` ).join( ', ' ) + ' }';
|
|
20
31
|
};
|
|
32
|
+
// Distribute the namespace in a map and assign it the next available color
|
|
33
|
+
const getColor = v =>
|
|
34
|
+
assignedColors.has( v ) ? assignedColors.get( v ) : assignedColors.set( v, colors[assignedColors.size % colors.length] ).get( v );
|
|
35
|
+
|
|
36
|
+
// Colorize a text using the namespace string
|
|
37
|
+
const colorizeByNamespace = ( namespace, text ) => `\x1b[38;5;${getColor( namespace )}m${text}\x1b[0m`;
|
|
21
38
|
|
|
22
39
|
// Development format: colorized with namespace prefix
|
|
23
40
|
const devFormat = winston.format.combine(
|
|
24
41
|
winston.format.colorize(),
|
|
25
42
|
winston.format.printf( ( { level, message, namespace, service: _, environment: __, ...rest } ) => {
|
|
26
|
-
const ns = namespace ?
|
|
43
|
+
const ns = 'Core' + ( namespace ? `.${namespace}` : '' );
|
|
27
44
|
const meta = formatMeta( rest );
|
|
28
|
-
return `[${level}] ${ns} ${message}${meta}`;
|
|
45
|
+
return `[${level}] ${colorizeByNamespace( ns, `${namespace}: ${message}` )}${meta}`;
|
|
29
46
|
} )
|
|
30
47
|
);
|
|
31
48
|
|
|
@@ -18,7 +18,7 @@ const addEntry = async ( entry, key ) => {
|
|
|
18
18
|
const client = await getRedisClient();
|
|
19
19
|
await client.multi()
|
|
20
20
|
.zAdd( key, [ { score: entry.timestamp, value: JSON.stringify( entry ) } ], { NX: true } )
|
|
21
|
-
.expire( key, getVars().redisIncompleteWorkflowsTTL
|
|
21
|
+
.expire( key, getVars().redisIncompleteWorkflowsTTL )
|
|
22
22
|
.exec();
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -88,6 +88,20 @@ describe( 'tracing/processors/s3', () => {
|
|
|
88
88
|
);
|
|
89
89
|
} );
|
|
90
90
|
|
|
91
|
+
it( 'exec(): sets expiry on the redis key for each entry', async () => {
|
|
92
|
+
const { exec } = await import( './index.js' );
|
|
93
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
94
|
+
const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
|
|
95
|
+
|
|
96
|
+
redisMulti.exec.mockResolvedValue( [] );
|
|
97
|
+
zRangeMock.mockResolvedValue( [ JSON.stringify( { phase: 'start', timestamp: startTime } ) ] );
|
|
98
|
+
|
|
99
|
+
await exec( { ...ctx, entry: { phase: 'start', timestamp: startTime, parentId: 'root' } } );
|
|
100
|
+
|
|
101
|
+
expect( redisMulti.expire ).toHaveBeenCalledTimes( 1 );
|
|
102
|
+
expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/WF/id1', 3600 );
|
|
103
|
+
} );
|
|
104
|
+
|
|
91
105
|
it( 'exec(): when buildTraceTree returns null (incomplete tree), does not upload or bust cache', async () => {
|
|
92
106
|
const { exec } = await import( './index.js' );
|
|
93
107
|
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
package/src/utils/index.d.ts
CHANGED
|
@@ -96,6 +96,14 @@ export function serializeBodyAndInferContentType( body: unknown ): SerializedBod
|
|
|
96
96
|
*/
|
|
97
97
|
export function isPlainObject( object: unknown ): boolean;
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Returns a copy of an array with its content shuffled.
|
|
101
|
+
*
|
|
102
|
+
* @param arr - The array to shuffle
|
|
103
|
+
* @returns A shuffled array copy
|
|
104
|
+
*/
|
|
105
|
+
export function shuffleArray( arr: array ): array;
|
|
106
|
+
|
|
99
107
|
/**
|
|
100
108
|
* Creates a new object by merging object `b` onto object `a`, biased toward `b`:
|
|
101
109
|
* - Fields in `b` overwrite fields in `a`.
|
package/src/utils/utils.js
CHANGED
|
@@ -162,6 +162,17 @@ export const serializeBodyAndInferContentType = payload => {
|
|
|
162
162
|
return { body: String( payload ), contentType: 'text/plain; charset=UTF-8' };
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Receives an array and returns a copy of it with the elements shuffled
|
|
167
|
+
*
|
|
168
|
+
* @param {array} arr
|
|
169
|
+
* @returns {array}
|
|
170
|
+
*/
|
|
171
|
+
export const shuffleArray = arr => arr
|
|
172
|
+
.map( v => ( { v, sort: Math.random() } ) )
|
|
173
|
+
.sort( ( a, b ) => a.sort - b.sort )
|
|
174
|
+
.map( ( { v } ) => v );
|
|
175
|
+
|
|
165
176
|
/**
|
|
166
177
|
* Creates a new object merging object "b" onto object "a" biased to "b":
|
|
167
178
|
* - Object "b" will overwrite fields on object "a";
|
|
@@ -2,9 +2,11 @@ import { Context } from '@temporalio/activity';
|
|
|
2
2
|
import { Storage } from '#async_storage';
|
|
3
3
|
import { addEventStart, addEventEnd, addEventError } from '#tracing';
|
|
4
4
|
import { headersToObject } from '../sandboxed_utils.js';
|
|
5
|
-
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
5
|
+
import { LifecycleEventLogMessage, LifecycleEvent, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
6
|
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
|
|
7
|
+
import { createChildLogger } from '#logger';
|
|
7
8
|
|
|
9
|
+
const log = createChildLogger( 'Activity' );
|
|
8
10
|
/*
|
|
9
11
|
This interceptor wraps every activity execution with cross-cutting concerns:
|
|
10
12
|
|
|
@@ -27,11 +29,15 @@ export class ActivityExecutionInterceptor {
|
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
async execute( input, next ) {
|
|
30
|
-
const { workflowExecution: { workflowId }, activityId, activityType } = Context.current().info;
|
|
32
|
+
const { workflowExecution: { workflowId }, activityId, activityType, workflowType: workflowName } = Context.current().info;
|
|
31
33
|
const { executionContext } = headersToObject( input.headers );
|
|
32
34
|
const { type: kind } = this.activities?.[activityType]?.[METADATA_ACCESS_SYMBOL];
|
|
33
35
|
|
|
36
|
+
const startDate = Date.now();
|
|
37
|
+
const logContext = { workflowName, workflowId, stepId: activityId, stepName: activityType };
|
|
34
38
|
const traceArguments = { kind, id: activityId, parentId: workflowId, name: activityType, executionContext };
|
|
39
|
+
|
|
40
|
+
log.info( LifecycleEventLogMessage[LifecycleEvent.START], logContext );
|
|
35
41
|
addEventStart( { details: input.args[0], ...traceArguments } );
|
|
36
42
|
|
|
37
43
|
const intervals = { heartbeat: null };
|
|
@@ -40,10 +46,13 @@ export class ActivityExecutionInterceptor {
|
|
|
40
46
|
intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
|
|
41
47
|
|
|
42
48
|
const output = await Storage.runWithContext( async _ => next( input ), { parentId: activityId, executionContext } );
|
|
49
|
+
|
|
50
|
+
log.info( LifecycleEventLogMessage[LifecycleEvent.END], { ...logContext, durationMs: Date.now() - startDate } );
|
|
43
51
|
addEventEnd( { details: output, ...traceArguments } );
|
|
44
52
|
return output;
|
|
45
53
|
|
|
46
54
|
} catch ( error ) {
|
|
55
|
+
log.error( LifecycleEventLogMessage[LifecycleEvent.ERROR], { ...logContext, durationMs: Date.now() - startDate, error: error.message } );
|
|
47
56
|
addEventError( { details: error, ...traceArguments } );
|
|
48
57
|
throw error;
|
|
49
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
|
-
const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
3
|
+
const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
|
|
4
4
|
|
|
5
5
|
const heartbeatMock = vi.fn();
|
|
6
6
|
const contextInfoMock = {
|
|
@@ -37,11 +37,14 @@ vi.mock( '../sandboxed_utils.js', () => ( {
|
|
|
37
37
|
headersToObject: () => ( { executionContext: { workflowId: 'wf-1' } } )
|
|
38
38
|
} ) );
|
|
39
39
|
|
|
40
|
-
vi.mock( '#consts',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
vi.mock( '#consts', async importOriginal => {
|
|
41
|
+
const actual = await importOriginal();
|
|
42
|
+
return {
|
|
43
|
+
...actual, get METADATA_ACCESS_SYMBOL() {
|
|
44
|
+
return METADATA_ACCESS_SYMBOL;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
} );
|
|
45
48
|
|
|
46
49
|
vi.mock( '../configs.js', () => ( {
|
|
47
50
|
get activityHeartbeatEnabled() {
|
package/src/worker/sinks.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { WORKFLOW_CATALOG } from '#consts';
|
|
1
|
+
import { LifecycleEvent, LifecycleEventLogMessage, WORKFLOW_CATALOG } from '#consts';
|
|
2
2
|
import { addEventStart, addEventEnd, addEventError } from '#tracing';
|
|
3
3
|
import { createChildLogger } from '#logger';
|
|
4
4
|
|
|
5
|
-
const log = createChildLogger( '
|
|
5
|
+
const log = createChildLogger( 'Workflow' );
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Adds a workflow trace event
|
|
9
9
|
*
|
|
10
10
|
* @param {function} method - Trace function to call
|
|
11
11
|
* @param {object} workflowInfo - Temporal workflowInfo object
|
|
@@ -20,44 +20,31 @@ const addWorkflowEvent = ( method, workflowInfo, details ) => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Logs the internal workflow event
|
|
24
24
|
*
|
|
25
|
-
* @param {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
25
|
+
* @param {LifecycleEvent} event
|
|
26
|
+
* @param {Object} workflowInfo
|
|
27
|
+
* @returns {void}
|
|
39
28
|
*/
|
|
40
|
-
const
|
|
29
|
+
const logWorkflowEvent = ( event, workflowInfo, error ) => {
|
|
41
30
|
const { workflowId, workflowType: workflowName, startTime } = workflowInfo;
|
|
31
|
+
// exclude internal catalog
|
|
42
32
|
if ( workflowName === WORKFLOW_CATALOG ) {
|
|
43
33
|
return;
|
|
44
34
|
}
|
|
45
|
-
const durationMs = Date.now() - startTime.getTime();
|
|
46
|
-
log.info( 'Workflow completed', { workflowName, workflowId, durationMs } );
|
|
47
|
-
};
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
if ( event === LifecycleEvent.START ) {
|
|
37
|
+
log.info( LifecycleEventLogMessage[event], { workflowName, workflowId } );
|
|
38
|
+
} else if ( event === LifecycleEvent.END ) {
|
|
39
|
+
log.info( LifecycleEventLogMessage[event], { workflowName, workflowId, durationMs: Date.now() - startTime.getTime() } );
|
|
40
|
+
} else if ( event === LifecycleEvent.ERROR ) {
|
|
41
|
+
log.info( LifecycleEventLogMessage[event], {
|
|
42
|
+
workflowName,
|
|
43
|
+
workflowId,
|
|
44
|
+
durationMs: Date.now() - startTime.getTime(),
|
|
45
|
+
error: error.message
|
|
46
|
+
} );
|
|
58
47
|
}
|
|
59
|
-
const durationMs = Date.now() - startTime.getTime();
|
|
60
|
-
log.error( 'Workflow failed', { workflowName, workflowId, durationMs } );
|
|
61
48
|
};
|
|
62
49
|
|
|
63
50
|
/**
|
|
@@ -77,25 +64,25 @@ const addEvent = ( method, workflowInfo, options ) => {
|
|
|
77
64
|
export const sinks = {
|
|
78
65
|
trace: {
|
|
79
66
|
addWorkflowEventStart: {
|
|
80
|
-
fn: ( workflowInfo,
|
|
81
|
-
|
|
82
|
-
addWorkflowEvent( addEventStart, workflowInfo,
|
|
67
|
+
fn: ( workflowInfo, details ) => {
|
|
68
|
+
logWorkflowEvent( LifecycleEvent.START, workflowInfo );
|
|
69
|
+
addWorkflowEvent( addEventStart, workflowInfo, details );
|
|
83
70
|
},
|
|
84
71
|
callDuringReplay: false
|
|
85
72
|
},
|
|
86
73
|
|
|
87
74
|
addWorkflowEventEnd: {
|
|
88
|
-
fn: ( workflowInfo,
|
|
89
|
-
|
|
90
|
-
addWorkflowEvent( addEventEnd, workflowInfo,
|
|
75
|
+
fn: ( workflowInfo, details ) => {
|
|
76
|
+
logWorkflowEvent( LifecycleEvent.END, workflowInfo );
|
|
77
|
+
addWorkflowEvent( addEventEnd, workflowInfo, details );
|
|
91
78
|
},
|
|
92
79
|
callDuringReplay: false
|
|
93
80
|
},
|
|
94
81
|
|
|
95
82
|
addWorkflowEventError: {
|
|
96
|
-
fn: ( workflowInfo,
|
|
97
|
-
|
|
98
|
-
addWorkflowEvent( addEventError, workflowInfo,
|
|
83
|
+
fn: ( workflowInfo, details ) => {
|
|
84
|
+
logWorkflowEvent( LifecycleEvent.ERROR, workflowInfo, details );
|
|
85
|
+
addWorkflowEvent( addEventError, workflowInfo, details );
|
|
99
86
|
},
|
|
100
87
|
callDuringReplay: false
|
|
101
88
|
},
|
package/src/logger.d.ts
DELETED