@output.ai/core 0.3.3 → 0.3.4

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.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,6 +39,7 @@
39
39
  "redis": "5.8.3",
40
40
  "stacktrace-parser": "0.1.11",
41
41
  "undici": "7.18.2",
42
+ "winston": "3.17.0",
42
43
  "zod": "4.1.12"
43
44
  },
44
45
  "license": "Apache-2.0",
@@ -48,6 +49,7 @@
48
49
  "imports": {
49
50
  "#consts": "./src/consts.js",
50
51
  "#errors": "./src/errors.js",
52
+ "#logger": "./src/logger.js",
51
53
  "#utils": "./src/utils/index.js",
52
54
  "#tracing": "./src/tracing/internal_interface.js",
53
55
  "#async_storage": "./src/async_storage.js",
@@ -4,6 +4,9 @@ import { setMetadata, isStringboolTrue, serializeFetchResponse, serializeBodyAnd
4
4
  import { ComponentType } from '#consts';
5
5
  import * as localProcessor from '../tracing/processors/local/index.js';
6
6
  import * as s3Processor from '../tracing/processors/s3/index.js';
7
+ import { createChildLogger } from '#logger';
8
+
9
+ const log = createChildLogger( 'HttpClient' );
7
10
 
8
11
  /**
9
12
  * Send a HTTP request.
@@ -42,7 +45,7 @@ export const sendHttpRequest = async ( { url, method, payload = undefined, heade
42
45
  }
43
46
  } )();
44
47
 
45
- console.log( '[Core.sendHttpRequest]', response.status, response.statusText );
48
+ log.info( 'HTTP request completed', { url, method, status: response.status, statusText: response.statusText } );
46
49
 
47
50
  if ( !response.ok ) {
48
51
  throw new FatalError( `${method} ${url} ${response.status}` );
@@ -0,0 +1,4 @@
1
+ import type { Logger } from 'winston';
2
+
3
+ export declare const logger: Logger;
4
+ export declare function createChildLogger( namespace: string ): Logger;
package/src/logger.js ADDED
@@ -0,0 +1,56 @@
1
+ import winston from 'winston';
2
+
3
+ const isProduction = process.env.NODE_ENV === 'production';
4
+
5
+ const levels = {
6
+ error: 0,
7
+ warn: 1,
8
+ info: 2,
9
+ http: 3,
10
+ debug: 4
11
+ };
12
+
13
+ // Format metadata as friendly JSON: "{ name: "foo", count: 5 }"
14
+ const formatMeta = obj => {
15
+ const entries = Object.entries( obj );
16
+ if ( !entries.length ) {
17
+ return '';
18
+ }
19
+ return ' { ' + entries.map( ( [ k, v ] ) => `${k}: ${JSON.stringify( v )}` ).join( ', ' ) + ' }';
20
+ };
21
+
22
+ // Development format: colorized with namespace prefix
23
+ const devFormat = winston.format.combine(
24
+ winston.format.colorize(),
25
+ winston.format.printf( ( { level, message, namespace, service: _, environment: __, ...rest } ) => {
26
+ const ns = namespace ? `{Core.${namespace}}` : '{Core}';
27
+ const meta = formatMeta( rest );
28
+ return `[${level}] ${ns} ${message}${meta}`;
29
+ } )
30
+ );
31
+
32
+ // Production format: structured JSON
33
+ const prodFormat = winston.format.combine(
34
+ winston.format.timestamp( { format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' } ),
35
+ winston.format.errors( { stack: true } ),
36
+ winston.format.json()
37
+ );
38
+
39
+ export const logger = winston.createLogger( {
40
+ levels,
41
+ level: isProduction ? 'info' : 'debug',
42
+ format: isProduction ? prodFormat : devFormat,
43
+ defaultMeta: {
44
+ service: 'output-worker',
45
+ environment: process.env.NODE_ENV || 'development'
46
+ },
47
+ transports: [ new winston.transports.Console() ]
48
+ } );
49
+
50
+ /**
51
+ * Creates a child logger with a specific namespace
52
+ *
53
+ * @param {string} namespace - The namespace for this logger (e.g., 'Scanner', 'Tracing')
54
+ * @returns {winston.Logger} Child logger instance with namespace metadata
55
+ */
56
+ export const createChildLogger = namespace => logger.child( { namespace } );
@@ -5,6 +5,9 @@ import { isStringboolTrue } from '#utils';
5
5
  import * as localProcessor from './processors/local/index.js';
6
6
  import * as s3Processor from './processors/s3/index.js';
7
7
  import { ComponentType } from '#consts';
8
+ import { createChildLogger } from '#logger';
9
+
10
+ const log = createChildLogger( 'Tracing' );
8
11
 
9
12
  const traceBus = new EventEmitter();
10
13
  const processors = [
@@ -32,7 +35,7 @@ export const init = async () => {
32
35
  try {
33
36
  await p.exec( ...args );
34
37
  } catch ( error ) {
35
- console.error( `[Tracing] "${p.name}" processor execution error.`, error );
38
+ log.error( 'Processor execution error', { processor: p.name, error: error.message, stack: error.stack } );
36
39
  }
37
40
  } );
38
41
  }
@@ -44,7 +47,7 @@ export const init = async () => {
44
47
  const serializeDetails = details => details instanceof Error ? serializeError( details ) : details;
45
48
 
46
49
  /**
47
- * Creates a new trace event phase and sens to be written
50
+ * Creates a new trace event phase and sends it to be written
48
51
  *
49
52
  * @param {string} phase - The phase
50
53
  * @param {object} fields - All the trace fields
@@ -2,6 +2,9 @@ import { z } from 'zod';
2
2
  import { dirname } from 'node:path';
3
3
  import { METADATA_ACCESS_SYMBOL } from '#consts';
4
4
  import { Catalog, CatalogActivity, CatalogWorkflow } from './catalog.js';
5
+ import { createChildLogger } from '#logger';
6
+
7
+ const log = createChildLogger( 'Catalog' );
5
8
 
6
9
  /**
7
10
  * Converts a Zod schema to JSON Schema format.
@@ -17,7 +20,7 @@ const convertToJsonSchema = schema => {
17
20
  try {
18
21
  return z.toJSONSchema( schema );
19
22
  } catch ( error ) {
20
- console.warn( 'Invalid schema provided (expected Zod schema):', schema, error );
23
+ log.warn( 'Invalid schema provided (expected Zod schema)', { error: error.message } );
21
24
  return null;
22
25
  }
23
26
  };
@@ -9,31 +9,34 @@ import { init as initTracing } from '#tracing';
9
9
  import { WORKFLOW_CATALOG } from '#consts';
10
10
  import { webpackConfigHook } from './bundler_options.js';
11
11
  import { initInterceptors } from './interceptors.js';
12
+ import { createChildLogger } from '#logger';
13
+
14
+ const log = createChildLogger( 'Worker' );
12
15
 
13
16
  // Get caller directory from command line arguments
14
17
  const callerDir = process.argv[2];
15
18
 
16
19
  ( async () => {
17
- console.log( '[Core]', 'Loading workflows...', { callerDir } );
20
+ log.info( 'Loading workflows...', { callerDir } );
18
21
  const workflows = await loadWorkflows( callerDir );
19
22
 
20
- console.log( '[Core]', 'Loading activities...', { callerDir } );
23
+ log.info( 'Loading activities...', { callerDir } );
21
24
  const activities = await loadActivities( callerDir, workflows );
22
25
 
23
- console.log( '[Core]', 'Creating worker entry point...' );
26
+ log.info( 'Creating worker entry point...' );
24
27
  const workflowsPath = createWorkflowsEntryPoint( workflows );
25
28
 
26
- console.log( '[Core]', 'Initializing tracing...' );
29
+ log.info( 'Initializing tracing...' );
27
30
  await initTracing();
28
31
 
29
- console.log( '[Core]', 'Creating workflows catalog...' );
32
+ log.info( 'Creating workflows catalog...' );
30
33
  const catalog = createCatalog( { workflows, activities } );
31
34
 
32
- console.log( '[Core]', 'Connecting Temporal...' );
33
- // enable TLS only when connecting remove (api key is present)
35
+ log.info( 'Connecting Temporal...' );
36
+ // Enable TLS when connecting to remote Temporal (API key present)
34
37
  const connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey } );
35
38
 
36
- console.log( '[Core]', 'Creating worker...' );
39
+ log.info( 'Creating worker...' );
37
40
  const worker = await Worker.create( {
38
41
  connection,
39
42
  namespace,
@@ -47,7 +50,7 @@ const callerDir = process.argv[2];
47
50
  bundlerOptions: { webpackConfigHook }
48
51
  } );
49
52
 
50
- console.log( '[Core]', 'Starting catalog workflow...' );
53
+ log.info( 'Starting catalog workflow...' );
51
54
  await new Client( { connection, namespace } ).workflow.start( WORKFLOW_CATALOG, {
52
55
  taskQueue,
53
56
  workflowId: catalogId, // use the name of the task queue as the catalog name, ensuring uniqueness
@@ -55,7 +58,7 @@ const callerDir = process.argv[2];
55
58
  args: [ catalog ]
56
59
  } );
57
60
 
58
- console.log( '[Core]', 'Running worker...' );
61
+ log.info( 'Running worker...' );
59
62
 
60
63
  // FORCE_QUIT_GRACE_MS delays the second instance of a shutdown command.
61
64
  // If running output-worker directly with npx, 2 signals are recieved in
@@ -69,12 +72,12 @@ const callerDir = process.argv[2];
69
72
  if ( elapsed < FORCE_QUIT_GRACE_MS ) {
70
73
  return; // ignore rapid duplicate signals
71
74
  }
72
- process.stderr.write( '[Core] Force quitting...\n' );
75
+ log.warn( 'Force quitting...' );
73
76
  process.exit( 1 );
74
77
  }
75
78
  state.isShuttingDown = true;
76
79
  state.shutdownStartedAt = Date.now();
77
- process.stderr.write( `[Core] Received ${signal}, shutting down...\n` );
80
+ log.info( 'Shutting down...', { signal } );
78
81
  worker.shutdown();
79
82
  };
80
83
 
@@ -82,13 +85,13 @@ const callerDir = process.argv[2];
82
85
  process.on( 'SIGINT', () => shutdown( 'SIGINT' ) );
83
86
 
84
87
  await worker.run();
85
- process.stderr.write( '[Core] Worker stopped.\n' );
88
+ log.info( 'Worker stopped.' );
86
89
 
87
90
  await connection.close();
88
- process.stderr.write( '[Core] Connection closed.\n' );
91
+ log.info( 'Connection closed.' );
89
92
 
90
93
  process.exit( 0 );
91
94
  } )().catch( error => {
92
- process.stderr.write( `[Core] Fatal error: ${error.message}\n` );
95
+ log.error( 'Fatal error', { message: error.message, stack: error.stack } );
93
96
  process.exit( 1 );
94
97
  } );
@@ -12,6 +12,9 @@ import {
12
12
  WORKFLOW_CATALOG,
13
13
  ACTIVITY_GET_TRACE_DESTINATIONS
14
14
  } from '#consts';
15
+ import { createChildLogger } from '#logger';
16
+
17
+ const log = createChildLogger( 'Scanner' );
15
18
 
16
19
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
17
20
 
@@ -64,7 +67,7 @@ export async function loadActivities( rootDir, workflows ) {
64
67
  for ( const { path: workflowPath, name: workflowName } of workflows ) {
65
68
  const dir = dirname( workflowPath );
66
69
  for await ( const { fn, metadata, path } of importComponents( dir, Object.values( activityMatchersBuilder( dir ) ) ) ) {
67
- console.log( '[Core.Scanner]', 'Component loaded:', metadata.type, metadata.name, 'at', path );
70
+ log.info( 'Component loaded', { type: metadata.type, name: metadata.name, path, workflow: workflowName } );
68
71
  // Activities loaded from a workflow path will use the workflow name as a namespace, which is unique across the platform, avoiding collision
69
72
  const activityKey = generateActivityKey( { namespace: workflowName, activityName: metadata.name } );
70
73
  activities[activityKey] = fn;
@@ -75,7 +78,7 @@ export async function loadActivities( rootDir, workflows ) {
75
78
 
76
79
  // Load shared activities/evaluators
77
80
  for await ( const { fn, metadata, path } of importComponents( rootDir, [ staticMatchers.sharedStepsDir, staticMatchers.sharedEvaluatorsDir ] ) ) {
78
- console.log( '[Core.Scanner]', 'Component loaded: shared ', metadata.type, metadata.name, 'at', path );
81
+ log.info( 'Shared component loaded', { type: metadata.type, name: metadata.name, path } );
79
82
  // The namespace for shared activities is fixed
80
83
  const activityKey = generateActivityKey( { namespace: SHARED_STEP_PREFIX, activityName: metadata.name } );
81
84
  activities[activityKey] = fn;
@@ -105,7 +108,7 @@ export async function loadWorkflows( rootDir ) {
105
108
  if ( staticMatchers.workflowPathHasShared( path ) ) {
106
109
  throw new Error( 'Workflow directory can\'t be named "shared"' );
107
110
  }
108
- console.log( '[Core.Scanner]', 'Workflow loaded:', metadata.name, 'at', path );
111
+ log.info( 'Workflow loaded', { name: metadata.name, path } );
109
112
  workflows.push( { ...metadata, path } );
110
113
  }
111
114
  return workflows;
@@ -1,12 +1,15 @@
1
1
  import { WORKFLOW_CATALOG } from '#consts';
2
2
  import { addEventStart, addEventEnd, addEventError } from '#tracing';
3
+ import { createChildLogger } from '#logger';
4
+
5
+ const log = createChildLogger( 'Worker' );
3
6
 
4
7
  /**
5
8
  * Start a workflow trace event
6
9
  *
7
10
  * @param {function} method - Trace function to call
8
11
  * @param {object} workflowInfo - Temporal workflowInfo object
9
- * @param {object} details - Teh details to attach to the event
12
+ * @param {object} details - The details to attach to the event
10
13
  */
11
14
  const addWorkflowEvent = ( method, workflowInfo, details ) => {
12
15
  const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
@@ -16,6 +19,47 @@ const addWorkflowEvent = ( method, workflowInfo, details ) => {
16
19
  method( { id, kind: 'workflow', name, details, parentId, executionContext } );
17
20
  };
18
21
 
22
+ /**
23
+ * Log workflow start
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
39
+ */
40
+ const logWorkflowEnd = workflowInfo => {
41
+ const { workflowId, workflowType: workflowName, startTime } = workflowInfo;
42
+ if ( workflowName === WORKFLOW_CATALOG ) {
43
+ return;
44
+ }
45
+ const durationMs = Date.now() - startTime.getTime();
46
+ log.info( 'Workflow completed', { workflowName, workflowId, durationMs } );
47
+ };
48
+
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;
58
+ }
59
+ const durationMs = Date.now() - startTime.getTime();
60
+ log.error( 'Workflow failed', { workflowName, workflowId, durationMs } );
61
+ };
62
+
19
63
  /**
20
64
  * Start a trace event with given configuration
21
65
  *
@@ -33,17 +77,26 @@ const addEvent = ( method, workflowInfo, options ) => {
33
77
  export const sinks = {
34
78
  trace: {
35
79
  addWorkflowEventStart: {
36
- fn: ( ...args ) => addWorkflowEvent( addEventStart, ...args ),
80
+ fn: ( workflowInfo, ...rest ) => {
81
+ logWorkflowStart( workflowInfo );
82
+ addWorkflowEvent( addEventStart, workflowInfo, ...rest );
83
+ },
37
84
  callDuringReplay: false
38
85
  },
39
86
 
40
87
  addWorkflowEventEnd: {
41
- fn: ( ...args ) => addWorkflowEvent( addEventEnd, ...args ),
88
+ fn: ( workflowInfo, ...rest ) => {
89
+ logWorkflowEnd( workflowInfo );
90
+ addWorkflowEvent( addEventEnd, workflowInfo, ...rest );
91
+ },
42
92
  callDuringReplay: false
43
93
  },
44
94
 
45
95
  addWorkflowEventError: {
46
- fn: ( ...args ) => addWorkflowEvent( addEventError, ...args ),
96
+ fn: ( workflowInfo, ...rest ) => {
97
+ logWorkflowError( workflowInfo );
98
+ addWorkflowEvent( addEventError, workflowInfo, ...rest );
99
+ },
47
100
  callDuringReplay: false
48
101
  },
49
102