@outputai/core 0.6.1-next.383b24b.0 → 0.6.1-next.5d7e612.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.6.1-next.383b24b.0",
3
+ "version": "0.6.1-next.5d7e612.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -65,7 +65,7 @@
65
65
  "#bus": "./src/bus.js",
66
66
  "#consts": "./src/consts.js",
67
67
  "#errors": "./src/errors.js",
68
- "#logger": "./src/logger.js",
68
+ "#logger": "./src/logger/index.js",
69
69
  "#utils": "./src/utils/index.js",
70
70
  "#internal_utils/*": "./src/internal_utils/*.js",
71
71
  "#tracing": "./src/tracing/internal_interface.js",
@@ -0,0 +1,61 @@
1
+ import { format, transports } from 'winston';
2
+ import { isPlainObject, shuffleArray } from '#utils';
3
+
4
+ /** Available colors enum */
5
+ const Color = {
6
+ Blue: '033',
7
+ Green: '030',
8
+ Orange: '208',
9
+ Turquoise: '045',
10
+ Purple: '129',
11
+ Yellow: '184'
12
+ };
13
+
14
+ /**
15
+ * Recursively format object as friendly JSON: { name: "foo", count: 5 }
16
+ *
17
+ * @param {any} v - The input value
18
+ * @returns {string} Formatted result
19
+ */
20
+ export const formatJson = v => {
21
+ if ( isPlainObject( v ) ) {
22
+ const entries = Object.entries( v );
23
+ return entries.length === 0 ? '{}' :
24
+ `{ ${entries.map( ( [ k, v ] ) => `${k}: ${formatJson( v )}` ).join( ', ' )} }`;
25
+ }
26
+ if ( Array.isArray( v ) ) {
27
+ return v.length === 0 ? '[]' : `[ ${v.map( p => formatJson( p ) ).join( ', ' )} ]`;
28
+ }
29
+ if ( typeof v === 'string' ) {
30
+ return JSON.stringify( v );
31
+ }
32
+ return v;
33
+ };
34
+
35
+ /** This instance color schema */
36
+ const COLORS = shuffleArray( Object.values( Color ) );
37
+
38
+ /** Stores all assigned colors per namespace. */
39
+ const assignedColors = new Map();
40
+
41
+ /**
42
+ * Get the previous assigned color for this value or if not present, assign a new one and store it
43
+ * @param {string} v - A text value
44
+ * @returns {string} The color
45
+ * */
46
+ const getColor = v =>
47
+ assignedColors.get( v ) ?? assignedColors.set( v, COLORS[assignedColors.size % COLORS.length] ).get( v );
48
+
49
+ export const options = {
50
+ level: 'debug',
51
+ transports: [ new transports.Console() ],
52
+ format: format.combine(
53
+ format.colorize(),
54
+ format.metadata(),
55
+ format.printf( ( { level, message, metadata } ) => {
56
+ const { namespace, ...fields } = metadata;
57
+ const jsonText = Object.keys( fields ).length > 0 ? formatJson( fields ) : null;
58
+ return `[${level}] \x1b[38;5;${getColor( namespace )}m${namespace}: ${message}\x1b[0m${jsonText ? ' ' + jsonText : ''}`;
59
+ } )
60
+ )
61
+ };
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ const LEVEL = Symbol.for( 'level' );
4
+ const MESSAGE = Symbol.for( 'message' );
5
+
6
+ vi.mock( '#utils', () => ( {
7
+ isPlainObject: v => Object.prototype.toString.call( v ) === '[object Object]',
8
+ shuffleArray: v => v
9
+ } ) );
10
+
11
+ const loadDevelopmentLogger = async () => {
12
+ vi.resetModules();
13
+ return import( './development.js' );
14
+ };
15
+
16
+ describe( 'logger/development', () => {
17
+ describe( 'formatJson', () => {
18
+ it( 'formats nested plain objects and arrays', async () => {
19
+ const { formatJson } = await loadDevelopmentLogger();
20
+
21
+ expect( formatJson( {
22
+ name: 'foo',
23
+ count: 5,
24
+ nested: { ok: true },
25
+ list: [ 1, 'two', { three: 3 } ]
26
+ } ) ).toBe( '{ name: "foo", count: 5, nested: { ok: true }, list: [ 1, "two", { three: 3 } ] }' );
27
+ } );
28
+
29
+ it( 'formats empty objects and arrays', async () => {
30
+ const { formatJson } = await loadDevelopmentLogger();
31
+
32
+ expect( formatJson( { emptyObject: {}, emptyArray: [] } ) ).toBe( '{ emptyObject: {}, emptyArray: [] }' );
33
+ } );
34
+
35
+ it( 'escapes string values', async () => {
36
+ const { formatJson } = await loadDevelopmentLogger();
37
+
38
+ expect( formatJson( { quote: 'hello "world"' } ) ).toBe( '{ quote: "hello \\"world\\"" }' );
39
+ } );
40
+ } );
41
+
42
+ describe( 'options.format', () => {
43
+ it( 'formats level, namespace, message, and metadata fields', async () => {
44
+ const { options } = await loadDevelopmentLogger();
45
+ const info = options.format.transform( {
46
+ [LEVEL]: 'info',
47
+ level: 'info',
48
+ message: 'Worker',
49
+ namespace: 'Telemetry',
50
+ status: { runState: 'RUNNING' },
51
+ memory: { heapUsed: 123 }
52
+ } );
53
+
54
+ expect( info[MESSAGE] ).toMatch( /^\[\x1b\[[\d;]+minfo\x1b\[[\d;]+m\] \x1b\[38;5;033mTelemetry: Worker\x1b\[0m /u );
55
+ expect( info[MESSAGE] ).toContain( '{ status: { runState: "RUNNING" }, memory: { heapUsed: 123 } }' );
56
+ } );
57
+
58
+ it( 'does not append metadata text when only namespace is present', async () => {
59
+ const { options } = await loadDevelopmentLogger();
60
+ const info = options.format.transform( {
61
+ [LEVEL]: 'debug',
62
+ level: 'debug',
63
+ message: 'Loading config...',
64
+ namespace: 'Worker'
65
+ } );
66
+
67
+ expect( info[MESSAGE] ).toMatch( /^\[\x1b\[[\d;]+mdebug\x1b\[[\d;]+m\] \x1b\[38;5;033mWorker: Loading config\.\.\.\x1b\[0m$/u );
68
+ } );
69
+ } );
70
+ } );
@@ -0,0 +1,14 @@
1
+ import winston from 'winston';
2
+
3
+ const { options } = await import( process.env.NODE_ENV === 'production' ? './production.js' : './development.js' );
4
+
5
+ // creates the root winston logger
6
+ const logger = winston.createLogger( options );
7
+
8
+ /**
9
+ * Creates a child logger with a specific namespace
10
+ *
11
+ * @param {string} namespace - The namespace for this logger (e.g., 'Scanner', 'Tracing')
12
+ * @returns {winston.Logger} Child logger instance with namespace metadata
13
+ */
14
+ export const createChildLogger = namespace => logger.child( { namespace } );
@@ -0,0 +1,27 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const loadLogger = async nodeEnv => {
4
+ vi.resetModules();
5
+ vi.stubEnv( 'NODE_ENV', nodeEnv );
6
+ return import( './index.js' );
7
+ };
8
+
9
+ describe( 'logger/index', () => {
10
+ afterEach( () => {
11
+ vi.unstubAllEnvs();
12
+ } );
13
+
14
+ it( 'loads the development logger options', async () => {
15
+ const { createChildLogger } = await loadLogger( 'development' );
16
+ const log = createChildLogger( 'Test' );
17
+
18
+ expect( typeof log.info ).toBe( 'function' );
19
+ } );
20
+
21
+ it( 'loads the production logger options', async () => {
22
+ const { createChildLogger } = await loadLogger( 'production' );
23
+ const log = createChildLogger( 'Test' );
24
+
25
+ expect( typeof log.info ).toBe( 'function' );
26
+ } );
27
+ } );
@@ -0,0 +1,15 @@
1
+ import { transports, format } from 'winston';
2
+
3
+ export const options = {
4
+ level: 'info',
5
+ transports: [ new transports.Console() ],
6
+ defaultMeta: {
7
+ service: 'output-worker',
8
+ environment: 'production'
9
+ },
10
+ format: format.combine(
11
+ format.timestamp( { format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' } ),
12
+ format.errors( { stack: true } ),
13
+ format.json()
14
+ )
15
+ };
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { options } from './production.js';
3
+
4
+ const LEVEL = Symbol.for( 'level' );
5
+ const MESSAGE = Symbol.for( 'message' );
6
+
7
+ describe( 'logger/production', () => {
8
+ it( 'uses info level and default production metadata', () => {
9
+ expect( options.level ).toBe( 'info' );
10
+ expect( options.defaultMeta ).toEqual( {
11
+ service: 'output-worker',
12
+ environment: 'production'
13
+ } );
14
+ } );
15
+
16
+ it( 'formats logs as JSON with timestamp and metadata fields', () => {
17
+ const info = options.format.transform( {
18
+ [LEVEL]: 'info',
19
+ level: 'info',
20
+ message: 'Worker',
21
+ namespace: 'Telemetry',
22
+ status: { runState: 'RUNNING' }
23
+ } );
24
+
25
+ const output = JSON.parse( info[MESSAGE] );
26
+
27
+ expect( output ).toMatchObject( {
28
+ level: 'info',
29
+ message: 'Worker',
30
+ namespace: 'Telemetry',
31
+ status: { runState: 'RUNNING' }
32
+ } );
33
+ expect( output.timestamp ).toEqual( expect.stringMatching( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/u ) );
34
+ } );
35
+
36
+ it( 'includes error stack when formatting Error messages', () => {
37
+ const error = new Error( 'boom' );
38
+ const info = options.format.transform( {
39
+ [LEVEL]: 'error',
40
+ level: 'error',
41
+ message: error
42
+ } );
43
+
44
+ const output = JSON.parse( info[MESSAGE] );
45
+
46
+ expect( output ).toMatchObject( {
47
+ level: 'error',
48
+ message: 'boom'
49
+ } );
50
+ expect( output.stack ).toContain( 'Error: boom' );
51
+ } );
52
+ } );
@@ -7,6 +7,7 @@ const coalesceEmptyString = v => v === '' ? undefined : v;
7
7
 
8
8
  const envVarSchema = z.object( {
9
9
  OUTPUT_CATALOG_ID: z.string().regex( /^[a-z0-9_.@-]+$/i ),
10
+ OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().nonnegative().default( 0 ) ),
10
11
  TEMPORAL_ADDRESS: z.string().default( 'localhost:7233' ),
11
12
  TEMPORAL_API_KEY: z.string().optional(),
12
13
  TEMPORAL_NAMESPACE: z.string().optional().default( 'default' ),
@@ -51,6 +52,7 @@ export const maxConcurrentWorkflowTaskPolls = envVars.TEMPORAL_MAX_CONCURRENT_WO
51
52
  export const namespace = envVars.TEMPORAL_NAMESPACE;
52
53
  export const taskQueue = envVars.OUTPUT_CATALOG_ID;
53
54
  export const catalogId = envVars.OUTPUT_CATALOG_ID;
55
+ export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVAL_MS;
54
56
  export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
55
57
  export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
56
58
  export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
@@ -10,6 +10,7 @@ const CONFIG_KEYS = [
10
10
  'TEMPORAL_MAX_CACHED_WORKFLOWS',
11
11
  'TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS',
12
12
  'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
13
+ 'OUTPUT_WORKER_TELEMETRY_INTERVAL_MS',
13
14
  'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
14
15
  'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED'
15
16
  ];
@@ -61,6 +62,7 @@ describe( 'worker/configs', () => {
61
62
  expect( configs.maxCachedWorkflows ).toBe( 1000 );
62
63
  expect( configs.maxConcurrentActivityTaskPolls ).toBe( 5 );
63
64
  expect( configs.maxConcurrentWorkflowTaskPolls ).toBe( 5 );
65
+ expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
64
66
  expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
65
67
  expect( configs.activityHeartbeatEnabled ).toBe( true );
66
68
  expect( configs.taskQueue ).toBe( 'test-catalog' );
@@ -74,11 +76,19 @@ describe( 'worker/configs', () => {
74
76
  expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 40 );
75
77
  } );
76
78
 
79
+ it( 'treats empty string for worker telemetry interval as default', async () => {
80
+ setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '' } );
81
+ const configs = await loadConfigs();
82
+
83
+ expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
84
+ } );
85
+
77
86
  it( 'parses custom numeric env vars', async () => {
78
87
  setEnv( {
79
88
  TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '10',
80
89
  TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS: '50',
81
90
  TEMPORAL_MAX_CACHED_WORKFLOWS: '500',
91
+ OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '30000',
82
92
  OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: '60000'
83
93
  } );
84
94
  const configs = await loadConfigs();
@@ -86,9 +96,24 @@ describe( 'worker/configs', () => {
86
96
  expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 10 );
87
97
  expect( configs.maxConcurrentWorkflowTaskExecutions ).toBe( 50 );
88
98
  expect( configs.maxCachedWorkflows ).toBe( 500 );
99
+ expect( configs.workerTelemetryIntervalMs ).toBe( 30000 );
89
100
  expect( configs.activityHeartbeatIntervalMs ).toBe( 60000 );
90
101
  } );
91
102
 
103
+ it( 'allows zero for worker telemetry interval', async () => {
104
+ setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '0' } );
105
+ const configs = await loadConfigs();
106
+
107
+ expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
108
+ } );
109
+
110
+ it( 'throws when worker telemetry interval is negative', async () => {
111
+ setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '-1' } );
112
+ vi.resetModules();
113
+
114
+ await expect( import( './configs.js' ) ).rejects.toThrow();
115
+ } );
116
+
92
117
  it( 'throws when optional number is zero or negative', async () => {
93
118
  setEnv( { TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '0' } );
94
119
  vi.resetModules();
@@ -11,9 +11,10 @@ import { registerShutdown } from './shutdown.js';
11
11
  import { startCatalog } from './start_catalog.js';
12
12
  import { bootstrapFetchProxy } from './proxy.js';
13
13
  import { messageBus } from '#bus';
14
- import './log_hooks.js';
15
14
  import { BusEventType } from '#consts';
16
15
  import { hashSourceCode } from './loader_tools.js';
16
+ import { setupTelemetry } from './setup_telemetry.js';
17
+ import './log_hooks.js';
17
18
 
18
19
  const log = createChildLogger( 'Worker' );
19
20
 
@@ -84,6 +85,8 @@ const callerDir = process.argv[2];
84
85
 
85
86
  registerShutdown( { worker, log } );
86
87
 
88
+ setupTelemetry( { worker } );
89
+
87
90
  log.info( 'Running worker...' );
88
91
  await Promise.all( [ worker.run(), startCatalog( { connection, namespace, catalog, catalogHash } ) ] );
89
92
 
@@ -62,6 +62,9 @@ vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: bootstrapFetchProxyMock }
62
62
  const registerShutdownMock = vi.fn();
63
63
  vi.mock( './shutdown.js', () => ( { registerShutdown: registerShutdownMock } ) );
64
64
 
65
+ const setupTelemetryMock = vi.fn();
66
+ vi.mock( './setup_telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
67
+
65
68
  vi.mock( './log_hooks.js', () => ( {} ) );
66
69
 
67
70
  const runState = { resolve: null };
@@ -129,6 +132,7 @@ describe( 'worker/index', () => {
129
132
  } ) );
130
133
  expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [], connection: mockConnection } );
131
134
  expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
135
+ expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
132
136
  expect( startCatalogMock ).toHaveBeenCalledWith( {
133
137
  connection: mockConnection,
134
138
  namespace: configValues.namespace,
@@ -0,0 +1,19 @@
1
+ import { createChildLogger } from '#logger';
2
+ import { workerTelemetryIntervalMs } from './configs.js';
3
+
4
+ const log = createChildLogger( 'Telemetry' );
5
+
6
+ export const setupTelemetry = ( { worker } ) => {
7
+ if ( workerTelemetryIntervalMs > 0 ) {
8
+ setInterval( () => {
9
+ log.info( 'Worker', {
10
+ status: worker.getStatus(),
11
+ memory: {
12
+ availableMemory: process.availableMemory(),
13
+ constrainedMemory: process.constrainedMemory(),
14
+ memoryUsage: process.memoryUsage()
15
+ }
16
+ } );
17
+ }, workerTelemetryIntervalMs ).unref();
18
+ }
19
+ };
@@ -0,0 +1,80 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const configMock = vi.hoisted( () => ( { workerTelemetryIntervalMs: 0 } ) );
4
+ const logMock = vi.hoisted( () => ( { info: vi.fn() } ) );
5
+ const createChildLoggerMock = vi.hoisted( () => vi.fn( () => logMock ) );
6
+
7
+ vi.mock( './configs.js', () => ( {
8
+ get workerTelemetryIntervalMs() {
9
+ return configMock.workerTelemetryIntervalMs;
10
+ }
11
+ } ) );
12
+
13
+ vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
14
+
15
+ const loadSetupTelemetry = async () => {
16
+ vi.resetModules();
17
+ return import( './setup_telemetry.js' );
18
+ };
19
+
20
+ const mockSetInterval = unrefMock =>
21
+ vi.spyOn( globalThis, 'setInterval' ).mockReturnValue( { unref: unrefMock } );
22
+
23
+ describe( 'worker/setup_telemetry', () => {
24
+ const availableMemoryMock = vi.fn();
25
+ const constrainedMemoryMock = vi.fn();
26
+ const memoryUsageMock = vi.fn();
27
+ const unrefMock = vi.fn();
28
+
29
+ beforeEach( () => {
30
+ vi.clearAllMocks();
31
+ configMock.workerTelemetryIntervalMs = 0;
32
+
33
+ availableMemoryMock.mockReturnValue( 1_000 );
34
+ constrainedMemoryMock.mockReturnValue( 2_000 );
35
+ memoryUsageMock.mockReturnValue( { heapUsed: 300 } );
36
+
37
+ vi.spyOn( process, 'availableMemory' ).mockImplementation( availableMemoryMock );
38
+ vi.spyOn( process, 'constrainedMemory' ).mockImplementation( constrainedMemoryMock );
39
+ vi.spyOn( process, 'memoryUsage' ).mockImplementation( memoryUsageMock );
40
+ } );
41
+
42
+ afterEach( () => {
43
+ vi.restoreAllMocks();
44
+ } );
45
+
46
+ it( 'does not create an interval when telemetry interval is disabled', async () => {
47
+ const setIntervalMock = mockSetInterval( unrefMock );
48
+ const { setupTelemetry } = await loadSetupTelemetry();
49
+
50
+ setupTelemetry( { worker: { getStatus: vi.fn() } } );
51
+
52
+ expect( setIntervalMock ).not.toHaveBeenCalled();
53
+ expect( logMock.info ).not.toHaveBeenCalled();
54
+ } );
55
+
56
+ it( 'logs worker status and memory on the configured interval', async () => {
57
+ const setIntervalMock = mockSetInterval( unrefMock );
58
+ configMock.workerTelemetryIntervalMs = 5_000;
59
+ const worker = { getStatus: vi.fn().mockReturnValue( { runState: 'RUNNING' } ) };
60
+ const { setupTelemetry } = await loadSetupTelemetry();
61
+
62
+ setupTelemetry( { worker } );
63
+
64
+ expect( createChildLoggerMock ).toHaveBeenCalledWith( 'Telemetry' );
65
+ expect( setIntervalMock ).toHaveBeenCalledWith( expect.any( Function ), 5_000 );
66
+ expect( unrefMock ).toHaveBeenCalled();
67
+
68
+ const [ callback ] = setIntervalMock.mock.calls[0];
69
+ callback();
70
+
71
+ expect( logMock.info ).toHaveBeenCalledWith( 'Worker', {
72
+ status: { runState: 'RUNNING' },
73
+ memory: {
74
+ availableMemory: 1_000,
75
+ constrainedMemory: 2_000,
76
+ memoryUsage: { heapUsed: 300 }
77
+ }
78
+ } );
79
+ } );
80
+ } );
package/src/logger.js DELETED
@@ -1,73 +0,0 @@
1
- import winston from 'winston';
2
- import { shuffleArray } from '#utils';
3
-
4
- const isProduction = process.env.NODE_ENV === 'production';
5
-
6
- const levels = {
7
- error: 0,
8
- warn: 1,
9
- info: 2,
10
- http: 3,
11
- debug: 4
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
-
24
- // Format metadata as friendly JSON: "{ name: "foo", count: 5 }"
25
- const formatMeta = obj => {
26
- const entries = Object.entries( obj );
27
- if ( !entries.length ) {
28
- return '';
29
- }
30
- return ' { ' + entries.map( ( [ k, v ] ) => `${k}: ${JSON.stringify( v )}` ).join( ', ' ) + ' }';
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`;
38
-
39
- // Development format: colorized with namespace prefix
40
- const devFormat = winston.format.combine(
41
- winston.format.colorize(),
42
- winston.format.printf( ( { level, message, namespace, service: _, environment: __, ...rest } ) => {
43
- const ns = 'Core' + ( namespace ? `.${namespace}` : '' );
44
- const meta = formatMeta( rest );
45
- return `[${level}] ${colorizeByNamespace( ns, `${namespace}: ${message}` )}${meta}`;
46
- } )
47
- );
48
-
49
- // Production format: structured JSON
50
- const prodFormat = winston.format.combine(
51
- winston.format.timestamp( { format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' } ),
52
- winston.format.errors( { stack: true } ),
53
- winston.format.json()
54
- );
55
-
56
- export const logger = winston.createLogger( {
57
- levels,
58
- level: isProduction ? 'info' : 'debug',
59
- format: isProduction ? prodFormat : devFormat,
60
- defaultMeta: {
61
- service: 'output-worker',
62
- environment: process.env.NODE_ENV || 'development'
63
- },
64
- transports: [ new winston.transports.Console() ]
65
- } );
66
-
67
- /**
68
- * Creates a child logger with a specific namespace
69
- *
70
- * @param {string} namespace - The namespace for this logger (e.g., 'Scanner', 'Tracing')
71
- * @returns {winston.Logger} Child logger instance with namespace metadata
72
- */
73
- export const createChildLogger = namespace => logger.child( { namespace } );