@output.ai/core 0.1.0 → 0.1.1

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.
Files changed (41) hide show
  1. package/README.md +16 -22
  2. package/package.json +8 -6
  3. package/src/consts.js +1 -1
  4. package/src/interface/evaluator.js +8 -4
  5. package/src/interface/workflow.js +11 -14
  6. package/src/internal_activities/index.js +0 -32
  7. package/src/tracing/index.d.ts +4 -4
  8. package/src/tracing/index.js +12 -121
  9. package/src/tracing/internal_interface.js +66 -0
  10. package/src/tracing/processors/local/index.js +50 -0
  11. package/src/tracing/processors/local/index.spec.js +67 -0
  12. package/src/tracing/processors/s3/index.js +51 -0
  13. package/src/tracing/processors/s3/index.spec.js +64 -0
  14. package/src/tracing/processors/s3/redis_client.js +19 -0
  15. package/src/tracing/processors/s3/redis_client.spec.js +50 -0
  16. package/src/tracing/processors/s3/s3_client.js +33 -0
  17. package/src/tracing/processors/s3/s3_client.spec.js +67 -0
  18. package/src/tracing/{tracer_tree.js → tools/build_trace_tree.js} +4 -11
  19. package/src/tracing/{tracer_tree.spec.js → tools/build_trace_tree.spec.js} +4 -20
  20. package/src/tracing/{utils.js → tools/utils.js} +7 -0
  21. package/src/tracing/trace_engine.js +63 -0
  22. package/src/tracing/trace_engine.spec.js +91 -0
  23. package/src/utils.js +8 -0
  24. package/src/worker/catalog_workflow/index.js +2 -1
  25. package/src/worker/catalog_workflow/index.spec.js +6 -10
  26. package/src/worker/configs.js +24 -0
  27. package/src/worker/index.js +7 -4
  28. package/src/worker/interceptors/activity.js +7 -14
  29. package/src/worker/interceptors/workflow.js +1 -2
  30. package/src/worker/loader.js +39 -30
  31. package/src/worker/loader.spec.js +20 -24
  32. package/src/worker/loader_tools.js +63 -0
  33. package/src/worker/loader_tools.spec.js +85 -0
  34. package/src/worker/sinks.js +8 -4
  35. package/src/configs.js +0 -31
  36. package/src/configs.spec.js +0 -331
  37. package/src/tracing/index.private.spec.js +0 -84
  38. package/src/tracing/index.public.spec.js +0 -86
  39. package/src/worker/internal_utils.js +0 -60
  40. package/src/worker/internal_utils.spec.js +0 -134
  41. /package/src/tracing/{utils.spec.js → tools/utils.spec.js} +0 -0
package/README.md CHANGED
@@ -23,26 +23,18 @@ Think that workflows is the orchestrator and steps are executors. So the workflo
23
23
  ### workflow.js
24
24
 
25
25
  ```js
26
- import { workflow } from '@output.ai/workflow';
26
+ import { workflow, z } from '@output.ai/workflow';
27
27
  import { guessByName } from './steps.js';
28
28
 
29
29
  export default workflow( {
30
30
  name: 'guessMyProfession',
31
31
  description: 'Guess a person profession by its name',
32
- inputSchema: {
33
- type: 'object',
34
- required: [ 'name' ],
35
- properties: {
36
- name: { type: 'string'}
37
- }
38
- },
39
- outputSchema: {
40
- type: 'object',
41
- required: [ 'profession' ],
42
- properties: {
43
- profession: { type: 'string'}
44
- }
45
- },
32
+ inputSchema: z.object( {
33
+ name: z.string()
34
+ } ),
35
+ outputSchema: z.object( {
36
+ profession: z.string()
37
+ } ),
46
38
  fn: async input => {
47
39
  const profession = await guessByName( input.name );
48
40
  return { profession };
@@ -57,12 +49,8 @@ import { api } from './api.js'
57
49
 
58
50
  export const guessByName = step( {
59
51
  name: 'guessByName',
60
- inputSchema: {
61
- type: 'string'
62
- },
63
- outputSchema: {
64
- type: 'string'
65
- },
52
+ inputSchema: z.string(),
53
+ outputSchema: z.string(),
66
54
  fn: async name => {
67
55
  const res = await api.consumer( name );
68
56
  return res.body;
@@ -138,4 +126,10 @@ Necessary env variables to run the worker locally:
138
126
  - `TEMPORAL_API_KEY`: The API key to access remote temporal. If using local temporal, leave it blank;
139
127
  - `CATALOG_ID`: The name of the local catalog, always set this. Use your email;
140
128
  - `API_AUTH_KEY`: The API key to access the Framework API. Local can be blank, remote use the proper API Key;
141
- - `TRACING_ENABLED`: A "stringbool" value indicating if traces should be generated or not;
129
+ - `TRACE_LOCAL_ON`: A "stringbool" value indicating if traces should be saved locally, needs REDIS_URL;
130
+ - `TRACE_REMOTE_ON`: A "stringbool" value indicating if traces should be saved remotely, needs REDIS_URL and AWS_* secrets;
131
+ - `REDIS_URL`: The redis address to connect. Only necessary when any type of trace is enabled;
132
+ - `TRACE_REMOTE_S3_BUCKET`: The AWS S3 bucket to send the traces. Only necessary when remote trace is enabled;
133
+ - `AWS_REGION`: AWS region to connect to send the traces, must match the bucket region. Only necessary when remote trace is enabled;
134
+ - `AWS_ACCESS_KEY_ID`: AWS key id. Only necessary when remote trace is enabled;
135
+ - `AWS_SECRET_ACCESS_KEY`: AWS secrete. Only necessary when remote trace is enabled;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -25,20 +25,22 @@
25
25
  "worker": "node ./src/worker/index.js"
26
26
  },
27
27
  "dependencies": {
28
+ "@aws-sdk/client-s3": "3.913.0",
28
29
  "@babel/generator": "7.28.3",
29
30
  "@babel/parser": "7.28.4",
30
31
  "@babel/traverse": "7.28.4",
31
32
  "@babel/types": "7.28.4",
32
- "@temporalio/worker": "1.13.0",
33
- "@temporalio/workflow": "1.13.0",
34
- "zod": "4.1.9"
33
+ "@temporalio/worker": "1.13.1",
34
+ "@temporalio/workflow": "1.13.1",
35
+ "redis": "5.8.3",
36
+ "zod": "4.1.12"
35
37
  },
36
38
  "license": "UNLICENSED",
37
39
  "imports": {
38
40
  "#consts": "./src/consts.js",
39
- "#configs": "./src/configs.js",
40
41
  "#errors": "./src/errors.js",
41
- "#tracing": "./src/tracing/index.js",
42
+ "#utils": "./src/utils.js",
43
+ "#tracing": "./src/tracing/internal_interface.js",
42
44
  "#async_storage": "./src/async_storage.js",
43
45
  "#internal_activities": "./src/internal_activities/index.js"
44
46
  }
package/src/consts.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const ACTIVITY_SEND_WEBHOOK = '__internal#sendWebhook';
2
- export const ACTIVITY_READ_TRACE_FILE = '__internal#readTraceFile';
3
2
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
4
3
  export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
4
+ export const WORKFLOW_CATALOG = '$catalog';
5
5
  export const ComponentType = {
6
6
  EVALUATOR: 'evaluator',
7
7
  INTERNAL_STEP: 'internal_step',
@@ -48,7 +48,8 @@ export class EvaluationResult {
48
48
  * @param {string} [args.reasoning] - The reasoning behind the result
49
49
  */
50
50
  constructor( args ) {
51
- if ( !EvaluationResult.#schema.safeParse( args ) ) {
51
+ const result = EvaluationResult.#schema.safeParse( args );
52
+ if ( result.error ) {
52
53
  throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
53
54
  }
54
55
  this.value = args.value;
@@ -73,7 +74,8 @@ export class EvaluationStringResult extends EvaluationResult {
73
74
  * @param {string} [args.reasoning] - The reasoning behind the result
74
75
  */
75
76
  constructor( args ) {
76
- if ( !EvaluationStringResult.#valueSchema.safeParse( args.value ) ) {
77
+ const result = EvaluationStringResult.#valueSchema.safeParse( args.value );
78
+ if ( result.error ) {
77
79
  throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
78
80
  }
79
81
  super( args );
@@ -96,7 +98,8 @@ export class EvaluationBooleanResult extends EvaluationResult {
96
98
  * @param {string} [args.reasoning] - The reasoning behind the result
97
99
  */
98
100
  constructor( args ) {
99
- if ( !EvaluationBooleanResult.#valueSchema.safeParse( args.value ) ) {
101
+ const result = EvaluationBooleanResult.#valueSchema.safeParse( args.value );
102
+ if ( result.error ) {
100
103
  throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
101
104
  }
102
105
  super( args );
@@ -119,7 +122,8 @@ export class EvaluationNumberResult extends EvaluationResult {
119
122
  * @param {string} [args.reasoning] - The reasoning behind the result
120
123
  */
121
124
  constructor( args ) {
122
- if ( !EvaluationNumberResult.#valueSchema.safeParse( args.value ) ) {
125
+ const result = EvaluationNumberResult.#valueSchema.safeParse( args.value );
126
+ if ( result.error ) {
123
127
  throw new EvaluationResultValidationError( z.prettifyError( result.error ) );
124
128
  }
125
129
  super( args );
@@ -5,7 +5,6 @@ import { setMetadata } from './metadata.js';
5
5
  import { FatalError, ValidationError } from '#errors';
6
6
  import { validateWorkflow } from './validations/static.js';
7
7
  import { validateWithSchema } from './validations/runtime.js';
8
- import { ACTIVITY_READ_TRACE_FILE } from '#consts';
9
8
 
10
9
  const temporalActivityConfigs = {
11
10
  startToCloseTimeout: '20 minute',
@@ -34,13 +33,18 @@ export function workflow( { name, description, inputSchema, outputSchema, fn } )
34
33
  return output;
35
34
  }
36
35
 
37
- const { workflowId, memo } = workflowInfo();
36
+ const { workflowId, memo, startTime } = workflowInfo();
38
37
 
39
- // keep trace id and helm from memo if present (child workflow)
40
- const traceId = memo.traceId ?? workflowId;
41
- const traceHelm = memo.traceHelm ?? name;
38
+ // Create the execution context object or preserve if it already exists:
39
+ // It will always contains the information about the root workflow
40
+ // It will be used to as context for tracing (connecting events)
41
+ const executionContext = memo.executionContext ?? {
42
+ workflowId,
43
+ workflowName: name,
44
+ startTime: startTime.getTime()
45
+ };
42
46
 
43
- Object.assign( memo, { traceId, traceHelm } );
47
+ Object.assign( memo, { executionContext } );
44
48
 
45
49
  // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
46
50
  const output = await fn.call( {
@@ -48,19 +52,12 @@ export function workflow( { name, description, inputSchema, outputSchema, fn } )
48
52
  invokeEvaluator: async ( evaluatorName, input ) => steps[`${workflowPath}#${evaluatorName}`]( input ),
49
53
 
50
54
  startWorkflow: async ( childName, input ) => {
51
- return executeChild( childName, { args: input ? [ input ] : [], memo: { traceId, traceHelm, parentId: workflowId } } );
55
+ return executeChild( childName, { args: input ? [ input ] : [], memo: { executionContext, parentId: workflowId } } );
52
56
  }
53
57
  }, input );
54
58
 
55
59
  validateWithSchema( outputSchema, output, `Workflow ${name} output` );
56
60
 
57
- // Adds trace file content to the output if it is the root workflow
58
- // @TODO this will be replaced in favor of a persistent storage elsewhere
59
- if ( !memo.parentId ) {
60
- const trace = await steps[ACTIVITY_READ_TRACE_FILE]( { traceId, traceHelm } );
61
- return { output, trace };
62
- }
63
-
64
61
  return output;
65
62
  };
66
63
 
@@ -1,11 +1,7 @@
1
1
  import { FatalError } from '#errors';
2
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
2
  import { setMetadata } from '../interface/metadata.js';
5
3
  import { ComponentType } from '#consts';
6
4
 
7
- const callerDir = process.argv[2];
8
-
9
5
  /**
10
6
  * Send a post to a given URL
11
7
  *
@@ -41,31 +37,3 @@ export const sendWebhook = async ( { url, workflowId, payload } ) => {
41
37
  };
42
38
 
43
39
  setMetadata( sendWebhook, { type: ComponentType.INTERNAL_STEP } );
44
-
45
- /**
46
- * Read the trace file of a given execution and returns the content
47
- *
48
- * @param {object} options
49
- * @param {string} options.traceId - The is of the trace
50
- * @param {string} options.traceHelm - The helm of the trace file
51
- * @returns {string[]} Each line of the trace file
52
- */
53
- export const readTraceFile = async ( { traceId, traceHelm } ) => {
54
- const dir = join( callerDir, 'logs', 'runs', traceHelm );
55
-
56
- if ( !existsSync( dir ) ) {
57
- console.log( '[Core.ReadTraceFile]', 'Trace folder not found', dir );
58
- return [];
59
- }
60
-
61
- const fileName = readdirSync( dir ).find( f => f.endsWith( `-${traceId}.raw` ) );
62
-
63
- if ( !fileName ) {
64
- console.log( '[Core.ReadTraceFile]', 'Trace file not found', { traceId, traceHelm } );
65
- return [];
66
- }
67
-
68
- return readFileSync( join( dir, fileName ), 'utf-8' ).split( '\n' );
69
- };
70
-
71
- setMetadata( readTraceFile, { type: ComponentType.INTERNAL_STEP, skipTrace: true } );
@@ -17,8 +17,8 @@ export declare const Tracing: {
17
17
  *
18
18
  * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
19
19
  * @param {string} kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
20
- * @param {string} name - The human friendly name of the Event: eg: query, request, create.
21
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
20
+ * @param {string} name - The human friendly name of the Event: query, request, create.
21
+ * @param {any} details - All details attached to this Event Phase. DB queried records, HTTP response body.
22
22
  * @returns {void}
23
23
  */
24
24
  addEventStart( args: { id: string; kind: string; name: string; details: any } ): void; // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -29,7 +29,7 @@ export declare const Tracing: {
29
29
  * It needs to use the same id of the start phase.
30
30
  *
31
31
  * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
32
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
32
+ * @param {any} details - All details attached to this Event Phase. DB queried records, HTTP response body.
33
33
  * @returns {void}
34
34
  */
35
35
  addEventEnd( args: { id: string; details: any } ): void; // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -40,7 +40,7 @@ export declare const Tracing: {
40
40
  * It needs to use the same id of the start phase.
41
41
  *
42
42
  * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
43
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
43
+ * @param {any} details - All details attached to this Event Phase. DB queried records, HTTP response body.
44
44
  * @returns {void}
45
45
  */
46
46
  addEventError( args: { id: string; details: any } ): void; // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -1,116 +1,4 @@
1
- import { Storage } from '#async_storage';
2
- import { mkdirSync, existsSync, readdirSync, appendFileSync } from 'node:fs';
3
- import { join } from 'path';
4
- import { EOL } from 'os';
5
- import { buildLogTree } from './tracer_tree.js';
6
- import { serializeError } from './utils.js';
7
-
8
- const callerDir = process.argv[2];
9
- // It is is isolated here instead of #configs to allow this module to be exported without all other configs requirements
10
- const tracingEnabled = [ '1', 'true', 'on' ].includes( process.env.TRACING_ENABLED );
11
-
12
- /**
13
- * Trace nomenclature
14
- *
15
- * Trace - The collection of Events;
16
- * Event - Any entry in the Trace file, must have the two phases START and END or ERROR;
17
- * Phase - An specific part of an Event, either START or the conclusive END or ERROR;
18
- */
19
-
20
- /**
21
- * Adds the Trace Event Phase to the trace file
22
- *
23
- * @param {string} phase - The phase
24
- * @param {object} options - All the trace fields
25
- * @returns {void}
26
- */
27
- function addEventPhase( phase, { kind, details, id, name, parentId, traceId, traceHelm } ) {
28
- if ( !tracingEnabled || name === 'catalog' ) {
29
- return;
30
- }
31
-
32
- const parsedDetails = details instanceof Error ? serializeError( details ) : details;
33
- const timestamp = Date.now();
34
- const entry = { phase, kind, details: parsedDetails, id, name, timestamp, parentId };
35
- const outputDir = join( callerDir, 'logs', 'runs', traceHelm );
36
-
37
- if ( !existsSync( outputDir ) ) {
38
- mkdirSync( outputDir, { recursive: true } );
39
- }
40
-
41
- const suffix = `-${traceId}.raw`;
42
- const logFile = readdirSync( outputDir ).find( f => f.endsWith( suffix ) ) ?? `${new Date( timestamp ).toISOString()}-${suffix}`;
43
- const logPath = join( outputDir, logFile );
44
-
45
- appendFileSync( logPath, JSON.stringify( entry ) + EOL, 'utf-8' );
46
- buildLogTree( logPath );
47
- };
48
-
49
- /**
50
- * Internal use only
51
- *
52
- * Adds the start phase of a new event at the default trace for the current workflow.
53
- *
54
- * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
55
- * @param {string} kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
56
- * @param {string} name - The human friendly name of the Event: eg: query, request, create.
57
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
58
- * @param {string} parentId - The parent Event, used to build a three.
59
- * @param {string} traceId - The traceId, which identifies from which trace this Event belongs.
60
- * @param {string} traceHelm - The trace helm, which is a taxonomical naming of the Trace.
61
- * @returns {void}
62
- */
63
- export const addEventStart = options => addEventPhase( 'start', options );
64
-
65
- /**
66
- * Internal use only
67
- *
68
- * Adds the end phase at an event at the default trace for the current workflow.
69
- *
70
- * It needs to use the same id of the start phase.
71
- *
72
- * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
73
- * @param {string} kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
74
- * @param {string} name - The human friendly name of the Event: eg: query, request, create.
75
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
76
- * @param {string} parentId - The parent Event, used to build a three.
77
- * @param {string} traceId - The traceId, which identifies from which trace this Event belongs.
78
- * @param {string} traceHelm - The trace helm, which is a taxonomical naming of the Trace.
79
- * @returns {void}
80
- */
81
- export const addEventEnd = options => addEventPhase( 'end', options );
82
-
83
- /**
84
- * Internal use only
85
- *
86
- * Adds the error phase at an event as error at the default trace for the current workflow.
87
- *
88
- * It needs to use the same id of the start phase.
89
- *
90
- * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
91
- * @param {string} kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
92
- * @param {string} name - The human friendly name of the Event: eg: query, request, create.
93
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
94
- * @param {string} parentId - The parent Event, used to build a three.
95
- * @param {string} traceId - The traceId, which identifies from which trace this Event belongs.
96
- * @param {string} traceHelm - The trace helm, which is a taxonomical naming of the Trace.
97
- * @returns {void}
98
- */
99
- export const addEventError = options => addEventPhase( 'error', options );
100
-
101
- /**
102
- * Adds an Event Phase reading traceId, traceHelm and parentId from the context.
103
- *
104
- * @param {object} options - The common trace configurations
105
- */
106
- function addEventPhaseWithContext( phase, options ) {
107
- const storeContent = Storage.load();
108
- if ( !storeContent ) { // means this was called from a unit test
109
- return;
110
- }
111
- const { parentId, traceId, traceHelm } = storeContent;
112
- addEventPhase( phase, { ...options, parentId, traceId, traceHelm } );
113
- };
1
+ import { addEventPhaseWithContext } from './trace_engine.js';
114
2
 
115
3
  /**
116
4
  * The public namespace for tracing
@@ -122,10 +10,11 @@ export const Tracing = {
122
10
  /**
123
11
  * Adds the start phase of a new event at the default trace for the current workflow.
124
12
  *
125
- * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
126
- * @param {string} kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
127
- * @param {string} name - The human friendly name of the Event: eg: query, request, create.
128
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
13
+ * @param {object} args
14
+ * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
15
+ * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
16
+ * @param {string} args.name - The human friendly name of the Event: query, request, create.
17
+ * @param {object} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
129
18
  * @returns {void}
130
19
  */
131
20
  addEventStart: ( { id, kind, name, details } ) => addEventPhaseWithContext( 'start', { kind, name, details, id } ),
@@ -135,8 +24,9 @@ export const Tracing = {
135
24
  *
136
25
  * It needs to use the same id of the start phase.
137
26
  *
138
- * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
139
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
27
+ * @param {object} args
28
+ * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
29
+ * @param {object} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
140
30
  * @returns {void}
141
31
  */
142
32
  addEventEnd: ( { id, details } ) => addEventPhaseWithContext( 'end', { id, details } ),
@@ -146,8 +36,9 @@ export const Tracing = {
146
36
  *
147
37
  * It needs to use the same id of the start phase.
148
38
  *
149
- * @param {string} id - A unique id for the Event, must be the same across all phases: start, end, error.
150
- * @param {any} details - All details attached to this Event Phase. Eg: DB queried records, HTTP response body.
39
+ * @param {object} args
40
+ * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
41
+ * @param {object} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
151
42
  * @returns {void}
152
43
  */
153
44
  addEventError: ( { id, details } ) => addEventPhaseWithContext( 'error', { id, details } )
@@ -0,0 +1,66 @@
1
+ import { addEventPhase, init } from './trace_engine.js';
2
+
3
+ /**
4
+ * Init method, if not called, no processors are attached and trace functions are dummy
5
+ */
6
+ export { init };
7
+
8
+ /**
9
+ * Trace nomenclature
10
+ *
11
+ * Trace - The collection of Events;
12
+ * Event - Any entry in the Trace file, must have the two phases START and END or ERROR;
13
+ * Phase - An specific part of an Event, either START or the conclusive END or ERROR;
14
+ */
15
+
16
+ /**
17
+ * Internal use only
18
+ *
19
+ * Adds the start phase of a new event at the default trace for the current workflow.
20
+ *
21
+ * @param {object} args
22
+ * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
23
+ * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
24
+ * @param {string} args.name - The human friendly name of the Event: query, request, create.
25
+ * @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
26
+ * @param {string} args.parentId - The parent Event, used to build a three.
27
+ * @param {object} args.executionContext - The original execution context from the workflow
28
+ * @returns {void}
29
+ */
30
+ export const addEventStart = options => addEventPhase( 'start', options );
31
+
32
+ /**
33
+ * Internal use only
34
+ *
35
+ * Adds the end phase at an event at the default trace for the current workflow.
36
+ *
37
+ * It needs to use the same id of the start phase.
38
+ *
39
+ * @param {object} args
40
+ * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
41
+ * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
42
+ * @param {string} args.name - The human friendly name of the Event: query, request, create.
43
+ * @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
44
+ * @param {string} args.parentId - The parent Event, used to build a three.
45
+ * @param {object} args.executionContext - The original execution context from the workflow
46
+ * @returns {void}
47
+ */
48
+ export const addEventEnd = options => addEventPhase( 'end', options );
49
+
50
+ /**
51
+ * Internal use only
52
+ *
53
+ * Adds the error phase at an event as error at the default trace for the current workflow.
54
+ *
55
+ * It needs to use the same id of the start phase.
56
+ *
57
+ * @param {object} args
58
+ * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
59
+ * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
60
+ * @param {string} args.name - The human friendly name of the Event: query, request, create.
61
+ * @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
62
+ * @param {string} args.parentId - The parent Event, used to build a three.
63
+ * @param {object} args.executionContext - The original execution context from the workflow
64
+ * @returns {void}
65
+ */
66
+ export const addEventError = options => addEventPhase( 'error', options );
@@ -0,0 +1,50 @@
1
+ import { appendFileSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'url';
4
+ import buildTraceTree from '../../tools/build_trace_tree.js';
5
+ import { EOL } from 'node:os';
6
+
7
+ const oneWeekInMS = 1000 * 60 * 60 * 24 * 7;
8
+ const __dirname = dirname( fileURLToPath( import.meta.url ) );
9
+ const tempDir = join( __dirname, 'temp', 'traces' );
10
+
11
+ const accumulate = ( { entry, executionContext: { workflowId, startTime } } ) => {
12
+ const path = join( tempDir, `${startTime}_${workflowId}.trace` );
13
+ appendFileSync( path, JSON.stringify( entry ) + EOL, 'utf-8' );
14
+ return readFileSync( path, 'utf-8' ).split( EOL ).slice( 0, -1 ).map( v => JSON.parse( v ) );
15
+ };
16
+
17
+ const cleanupOldTempFiles = ( threshold = Date.now() - oneWeekInMS ) =>
18
+ readdirSync( tempDir )
19
+ .filter( f => +f.split( '_' )[0] < threshold )
20
+ .forEach( f => rmSync( join( tempDir, f ) ) );
21
+
22
+ /**
23
+ * Init this processor
24
+ */
25
+ export const init = () => {
26
+ mkdirSync( tempDir, { recursive: true } );
27
+ cleanupOldTempFiles();
28
+ };
29
+
30
+ /**
31
+ * Execute this processor:
32
+ *
33
+ * Persist a trace tree file to local file system, updating upon each new entry
34
+ *
35
+ * @param {object} args
36
+ * @param {object} entry - Trace event phase
37
+ * @param {object} executionContext - Execution info: workflowId, workflowName, startTime
38
+ * @returns
39
+ */
40
+ export const exec = ( { entry, executionContext } ) => {
41
+ const { workflowId, workflowName, startTime } = executionContext;
42
+ const content = buildTraceTree( accumulate( { entry, executionContext } ) );
43
+
44
+ const timestamp = new Date( startTime ).toISOString().replace( /[:T.]/g, '-' );
45
+ const dir = join( process.argv[2], 'logs', 'runs', workflowName );
46
+ const path = join( dir, `${timestamp}_${workflowId}.json` );
47
+
48
+ mkdirSync( dir, { recursive: true } );
49
+ writeFileSync( path, JSON.stringify( content, undefined, 2 ) + EOL, 'utf-8' );
50
+ };
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // In-memory fs mock store
4
+ const store = { files: new Map() };
5
+ const mkdirSyncMock = vi.fn();
6
+ const writeFileSyncMock = vi.fn();
7
+ const appendFileSyncMock = vi.fn( ( path, data ) => {
8
+ const prev = store.files.get( path ) ?? '';
9
+ store.files.set( path, prev + data );
10
+ } );
11
+ const readFileSyncMock = vi.fn( path => store.files.get( path ) ?? '' );
12
+ const readdirSyncMock = vi.fn( () => [] );
13
+ const rmSyncMock = vi.fn();
14
+
15
+ vi.mock( 'node:fs', () => ( {
16
+ mkdirSync: mkdirSyncMock,
17
+ writeFileSync: writeFileSyncMock,
18
+ appendFileSync: appendFileSyncMock,
19
+ readFileSync: readFileSyncMock,
20
+ readdirSync: readdirSyncMock,
21
+ rmSync: rmSyncMock
22
+ } ) );
23
+
24
+ const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
25
+ vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
26
+
27
+ describe( 'tracing/processors/local', () => {
28
+ beforeEach( () => {
29
+ vi.clearAllMocks();
30
+ store.files.clear();
31
+ process.argv[2] = '/tmp/project';
32
+ } );
33
+
34
+ it( 'init(): creates temp dir and cleans up old files', async () => {
35
+ const { init } = await import( './index.js' );
36
+
37
+ const now = Date.now();
38
+ readdirSyncMock.mockReturnValue( [ `${now - ( 8 * 24 * 60 * 60 * 1000 )}_old.trace`, `${now}_new.trace` ] );
39
+
40
+ init();
41
+
42
+ expect( mkdirSyncMock ).toHaveBeenCalledWith( expect.stringMatching( /temp\/traces$/ ), { recursive: true } );
43
+ expect( rmSyncMock ).toHaveBeenCalledTimes( 1 );
44
+ } );
45
+
46
+ it( 'exec(): accumulates entries and writes aggregated tree', async () => {
47
+ const { exec, init } = await import( './index.js' );
48
+ init();
49
+
50
+ const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
51
+ const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
52
+
53
+ exec( { ...ctx, entry: { name: 'A', phase: 'start', timestamp: startTime } } );
54
+ exec( { ...ctx, entry: { name: 'A', phase: 'tick', timestamp: startTime + 1 } } );
55
+ exec( { ...ctx, entry: { name: 'A', phase: 'end', timestamp: startTime + 2 } } );
56
+
57
+ // buildTraceTree called with 1, 2, 3 entries respectively
58
+ expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 3 );
59
+ expect( buildTraceTreeMock.mock.calls.at( -1 )[0].length ).toBe( 3 );
60
+
61
+ expect( writeFileSyncMock ).toHaveBeenCalledTimes( 3 );
62
+ const [ writtenPath, content ] = writeFileSyncMock.mock.calls.at( -1 );
63
+ expect( writtenPath ).toMatch( /\/logs\/runs\/WF\// );
64
+ expect( JSON.parse( content.trim() ).count ).toBe( 3 );
65
+ } );
66
+ } );
67
+
@@ -0,0 +1,51 @@
1
+ import { upload } from './s3_client.js';
2
+ import { getRedisClient } from './redis_client.js';
3
+ import buildTraceTree from '../../tools/build_trace_tree.js';
4
+ import { EOL } from 'node:os';
5
+
6
+ const oneMonthInSeconds = 60 * 60 * 24 * 30;
7
+
8
+ const accumulate = async ( { entry, executionContext: { workflowName, workflowId } } ) => {
9
+ const key = `traces/${workflowName}/${workflowId}`;
10
+ const transaction = ( await getRedisClient() ).multi();
11
+
12
+ transaction.zAdd( key, [
13
+ { score: entry.timestamp, value: JSON.stringify( entry ) }
14
+ ], { NX: true } );
15
+ transaction.expire( key, oneMonthInSeconds, 'GT' );
16
+ transaction.zRange( key, 0, -1 );
17
+ const [ ,, zList ] = await transaction.exec();
18
+ return zList.map( v => JSON.parse( v ) );
19
+ };
20
+
21
+ const getS3Key = ( { startTime, workflowId, workflowName } ) => {
22
+ const isoDate = new Date( startTime ).toISOString();
23
+ const [ year, month, day ] = isoDate.split( /\D/, 3 );
24
+ const timeStamp = isoDate.replace( /[:T.]/g, '-' );
25
+ return `${workflowName}/${year}/${month}/${day}/${timeStamp}_${workflowId}.json`;
26
+ };
27
+
28
+ /**
29
+ * Init this processor
30
+ */
31
+ export const init = async () => {
32
+ await getRedisClient();
33
+ };
34
+
35
+ /**
36
+ * Execute this processor: send a complete trace tree file to S3 when the workflow finishes
37
+ *
38
+ * @param {object} args
39
+ * @param {object} entry - Trace event phase
40
+ * @param {object} executionContext - Execution info: workflowId, workflowName, startTime
41
+ */
42
+ export const exec = async ( { entry, executionContext } ) => {
43
+ const { workflowName, workflowId, startTime } = executionContext;
44
+ const content = buildTraceTree( await accumulate( { entry, executionContext } ) );
45
+
46
+ const isRootWorkflowEnd = !entry.parentId && entry.phase !== 'start';
47
+ return isRootWorkflowEnd ? upload( {
48
+ key: getS3Key( { workflowId, workflowName, startTime } ),
49
+ content: JSON.stringify( content, undefined, 2 ) + EOL
50
+ } ) : 0;
51
+ };