@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
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
@@ -17,7 +17,7 @@ export {
17
17
  EvaluationStringResult,
18
18
  EvaluationBooleanResult,
19
19
  EvaluationFeedback,
20
- // webhook tools
20
+ // tools
21
21
  executeInParallel,
22
22
  sendHttpRequest,
23
23
  sendPostRequestAndAwaitWebhook,
@@ -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( '#utils', () => {
8
- return {
9
- setMetadata: vi.fn(),
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 ? `{Core.${namespace}}` : '{Core}';
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, 'GT' )
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' );
@@ -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`.
@@ -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
- get METADATA_ACCESS_SYMBOL() {
42
- return METADATA_ACCESS_SYMBOL;
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() {
@@ -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( 'Worker' );
5
+ const log = createChildLogger( 'Workflow' );
6
6
 
7
7
  /**
8
- * Start a workflow trace event
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
- * Log workflow start
23
+ * Logs the internal workflow event
24
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
25
+ * @param {LifecycleEvent} event
26
+ * @param {Object} workflowInfo
27
+ * @returns {void}
39
28
  */
40
- const logWorkflowEnd = workflowInfo => {
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
- * 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;
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, ...rest ) => {
81
- logWorkflowStart( workflowInfo );
82
- addWorkflowEvent( addEventStart, workflowInfo, ...rest );
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, ...rest ) => {
89
- logWorkflowEnd( workflowInfo );
90
- addWorkflowEvent( addEventEnd, workflowInfo, ...rest );
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, ...rest ) => {
97
- logWorkflowError( workflowInfo );
98
- addWorkflowEvent( addEventError, workflowInfo, ...rest );
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
@@ -1,4 +0,0 @@
1
- import type { Logger } from 'winston';
2
-
3
- export declare const logger: Logger;
4
- export declare function createChildLogger( namespace: string ): Logger;