@outputai/core 0.6.0 → 0.6.1-dev.aab2335.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 +2 -2
- package/src/activity_integration/context.d.ts +5 -9
- package/src/activity_integration/context.js +5 -4
- package/src/activity_integration/context.spec.js +10 -15
- package/src/activity_integration/events.d.ts +2 -4
- package/src/activity_integration/events.js +8 -3
- package/src/activity_integration/events.spec.js +58 -29
- package/src/bus.js +18 -9
- package/src/bus.spec.js +30 -0
- package/src/hooks/index.d.ts +112 -58
- package/src/hooks/index.js +15 -12
- package/src/hooks/index.spec.js +60 -32
- package/src/interface/workflow.js +19 -35
- package/src/interface/workflow.spec.js +104 -15
- package/src/internal_activities/index.js +3 -3
- package/src/internal_activities/index.spec.js +31 -1
- package/src/internal_utils/temporal_context.js +12 -0
- package/src/internal_utils/temporal_context.spec.ts +83 -0
- package/src/internal_utils/trace_info.js +21 -0
- package/src/internal_utils/trace_info.spec.js +47 -0
- package/src/internal_utils/workflow_context.js +29 -0
- package/src/internal_utils/workflow_context.spec.js +46 -0
- package/src/logger/development.js +61 -0
- package/src/logger/development.spec.js +70 -0
- package/src/logger/index.js +14 -0
- package/src/logger/index.spec.js +27 -0
- package/src/logger/production.js +15 -0
- package/src/logger/production.spec.js +52 -0
- package/src/tracing/internal_interface.js +4 -4
- package/src/tracing/processors/local/index.js +21 -26
- package/src/tracing/processors/local/index.spec.js +39 -45
- package/src/tracing/processors/s3/index.js +13 -23
- package/src/tracing/processors/s3/index.spec.js +33 -26
- package/src/tracing/trace_attribute.js +0 -1
- package/src/tracing/trace_engine.js +8 -12
- package/src/tracing/trace_engine.spec.js +31 -27
- package/src/worker/configs.js +2 -0
- package/src/worker/configs.spec.js +25 -0
- package/src/worker/index.js +4 -1
- package/src/worker/index.spec.js +4 -0
- package/src/worker/interceptors/activity.js +31 -29
- package/src/worker/interceptors/activity.spec.js +58 -26
- package/src/worker/interceptors/workflow.js +7 -2
- package/src/worker/interceptors/workflow.spec.js +42 -6
- package/src/worker/log_hooks.js +35 -46
- package/src/worker/log_hooks.spec.js +43 -46
- package/src/worker/setup_telemetry.js +19 -0
- package/src/worker/setup_telemetry.spec.js +80 -0
- package/src/worker/sinks.js +24 -24
- package/src/interface/workflow_context.js +0 -33
- package/src/logger.js +0 -73
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { TraceInfo } from './trace_info.js';
|
|
3
|
+
|
|
4
|
+
const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
|
|
5
|
+
const workflowInfoMock = vi.hoisted( () => vi.fn() );
|
|
6
|
+
|
|
7
|
+
vi.mock( '@temporalio/workflow', () => ( {
|
|
8
|
+
inWorkflowContext: inWorkflowContextMock,
|
|
9
|
+
workflowInfo: workflowInfoMock
|
|
10
|
+
} ) );
|
|
11
|
+
|
|
12
|
+
describe( 'TraceInfo', () => {
|
|
13
|
+
beforeEach( () => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
it( 'builds trace info from Temporal workflow info in workflow context', () => {
|
|
18
|
+
inWorkflowContextMock.mockReturnValue( true );
|
|
19
|
+
workflowInfoMock.mockReturnValue( {
|
|
20
|
+
workflowId: 'workflow-id',
|
|
21
|
+
workflowType: 'workflow-type',
|
|
22
|
+
runId: 'run-id',
|
|
23
|
+
startTime: new Date( '2026-06-02T09:00:00.000Z' )
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
expect( TraceInfo.build( { disableTrace: false } ) ).toEqual( {
|
|
27
|
+
workflowId: 'workflow-id',
|
|
28
|
+
workflowType: 'workflow-type',
|
|
29
|
+
runId: 'run-id',
|
|
30
|
+
startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
|
|
31
|
+
disableTrace: false
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
it( 'builds trace info without Temporal fields outside workflow context', () => {
|
|
36
|
+
inWorkflowContextMock.mockReturnValue( false );
|
|
37
|
+
|
|
38
|
+
expect( TraceInfo.build( { disableTrace: true } ) ).toEqual( {
|
|
39
|
+
workflowId: undefined,
|
|
40
|
+
workflowType: undefined,
|
|
41
|
+
runId: undefined,
|
|
42
|
+
startTime: undefined,
|
|
43
|
+
disableTrace: true
|
|
44
|
+
} );
|
|
45
|
+
expect( workflowInfoMock ).not.toHaveBeenCalled();
|
|
46
|
+
} );
|
|
47
|
+
} );
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { workflowInfo, continueAsNew, inWorkflowContext } from '@temporalio/workflow';
|
|
2
|
+
|
|
3
|
+
export class WorkflowContext {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a new context instance
|
|
7
|
+
* @returns {object} context
|
|
8
|
+
*/
|
|
9
|
+
static build() {
|
|
10
|
+
if ( !inWorkflowContext() ) {
|
|
11
|
+
return {
|
|
12
|
+
control: {
|
|
13
|
+
continueAsNew: async () => {},
|
|
14
|
+
isContinueAsNewSuggested: () => false
|
|
15
|
+
},
|
|
16
|
+
info: { workflowId: 'test-workflow', runId: 'test-run' }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { workflowId, runId } = workflowInfo();
|
|
21
|
+
return {
|
|
22
|
+
control: {
|
|
23
|
+
continueAsNew,
|
|
24
|
+
isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested
|
|
25
|
+
},
|
|
26
|
+
info: { workflowId, runId }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { WorkflowContext } from './workflow_context.js';
|
|
3
|
+
|
|
4
|
+
const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
|
|
5
|
+
const workflowInfoMock = vi.hoisted( () => vi.fn() );
|
|
6
|
+
const continueAsNewMock = vi.hoisted( () => vi.fn() );
|
|
7
|
+
|
|
8
|
+
vi.mock( '@temporalio/workflow', () => ( {
|
|
9
|
+
continueAsNew: continueAsNewMock,
|
|
10
|
+
inWorkflowContext: inWorkflowContextMock,
|
|
11
|
+
workflowInfo: workflowInfoMock
|
|
12
|
+
} ) );
|
|
13
|
+
|
|
14
|
+
describe( 'WorkflowContext', () => {
|
|
15
|
+
beforeEach( () => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
} );
|
|
18
|
+
|
|
19
|
+
it( 'builds a test context outside Temporal workflow context', async () => {
|
|
20
|
+
inWorkflowContextMock.mockReturnValue( false );
|
|
21
|
+
|
|
22
|
+
const context = WorkflowContext.build();
|
|
23
|
+
|
|
24
|
+
expect( context.info ).toEqual( { workflowId: 'test-workflow', runId: 'test-run' } );
|
|
25
|
+
expect( context.control.isContinueAsNewSuggested() ).toBe( false );
|
|
26
|
+
await expect( context.control.continueAsNew() ).resolves.toBeUndefined();
|
|
27
|
+
expect( workflowInfoMock ).not.toHaveBeenCalled();
|
|
28
|
+
expect( continueAsNewMock ).not.toHaveBeenCalled();
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
it( 'builds a workflow context from Temporal workflow info', () => {
|
|
32
|
+
inWorkflowContextMock.mockReturnValue( true );
|
|
33
|
+
workflowInfoMock.mockReturnValue( {
|
|
34
|
+
workflowId: 'workflow-id',
|
|
35
|
+
runId: 'run-id',
|
|
36
|
+
continueAsNewSuggested: true
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
const context = WorkflowContext.build();
|
|
40
|
+
|
|
41
|
+
expect( context.info ).toEqual( { workflowId: 'workflow-id', runId: 'run-id' } );
|
|
42
|
+
expect( context.control.continueAsNew ).toBe( continueAsNewMock );
|
|
43
|
+
expect( context.control.isContinueAsNewSuggested() ).toBe( true );
|
|
44
|
+
expect( workflowInfoMock ).toHaveBeenCalledTimes( 2 );
|
|
45
|
+
} );
|
|
46
|
+
} );
|
|
@@ -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
|
+
} );
|
|
@@ -32,7 +32,7 @@ export { EventAction };
|
|
|
32
32
|
* @param {string} args.name - The human-friendly name of the Event: query, request, create.
|
|
33
33
|
* @param {any} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
34
34
|
* @param {string} args.parentId - The parent Event, used to build a tree.
|
|
35
|
-
* @param {object} args.
|
|
35
|
+
* @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
|
|
36
36
|
* @returns {void}
|
|
37
37
|
*/
|
|
38
38
|
export const addEventStart = options => addEventAction( EventAction.START, options );
|
|
@@ -45,7 +45,7 @@ export const addEventStart = options => addEventAction( EventAction.START, optio
|
|
|
45
45
|
* @param {object} args
|
|
46
46
|
* @param {string} args.id - The id of the event to conclude.
|
|
47
47
|
* @param {any} args.details - Arbitrary data to add to the event; it is used as the "output" field.
|
|
48
|
-
* @param {object} args.
|
|
48
|
+
* @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
|
|
49
49
|
* @returns {void}
|
|
50
50
|
*/
|
|
51
51
|
export const addEventEnd = options => addEventAction( EventAction.END, options );
|
|
@@ -58,7 +58,7 @@ export const addEventEnd = options => addEventAction( EventAction.END, options )
|
|
|
58
58
|
* @param {object} args
|
|
59
59
|
* @param {string} args.id - The id of the event to conclude.
|
|
60
60
|
* @param {any} args.details - Arbitrary data to add to the event; it is used as the "error" field.
|
|
61
|
-
* @param {object} args.
|
|
61
|
+
* @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
|
|
62
62
|
* @returns {void}
|
|
63
63
|
*/
|
|
64
64
|
export const addEventError = options => addEventAction( EventAction.ERROR, options );
|
|
@@ -71,7 +71,7 @@ export const addEventError = options => addEventAction( EventAction.ERROR, optio
|
|
|
71
71
|
* @param {object} args
|
|
72
72
|
* @param {string} args.id - The id of the event to attach the attribute to.
|
|
73
73
|
* @param {object} args.details - The attribute to add to this event, must be in `{ name: string, value: any }` format.
|
|
74
|
-
* @param {object} args.
|
|
74
|
+
* @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
|
|
75
75
|
* @returns {void}
|
|
76
76
|
*/
|
|
77
77
|
export const addEventAttribute = options => addEventAction( EventAction.ADD_ATTR, options );
|
|
@@ -19,10 +19,10 @@ const tempTraceFilesDir = join( __dirname, 'temp', 'traces' );
|
|
|
19
19
|
/**
|
|
20
20
|
* Builds the temp file path to accumulate trace entries
|
|
21
21
|
*
|
|
22
|
-
* @param {object}
|
|
22
|
+
* @param {object} traceInfo - Trace information object
|
|
23
23
|
* @returns {string}
|
|
24
24
|
*/
|
|
25
|
-
const createTempFilePath = ( {
|
|
25
|
+
const createTempFilePath = ( { startTime, runId } ) => join( tempTraceFilesDir, `${startTime}_${runId}.trace` );
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Adds a trace entry to the accumulation file.
|
|
@@ -49,18 +49,18 @@ const cleanupOldTempFiles = ( threshold = Date.now() - tempFilesTTL ) =>
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Resolves the deep folder structure that stores a workflow trace.
|
|
52
|
-
* @param {string}
|
|
52
|
+
* @param {string} workflowType
|
|
53
53
|
* @returns {string}
|
|
54
54
|
*/
|
|
55
|
-
const resolveTraceFolder =
|
|
55
|
+
const resolveTraceFolder = workflowType => join( 'runs', workflowType );
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Resolves the local file system path for ALL file I/O operations (read/write)
|
|
59
59
|
* Uses the project root path
|
|
60
|
-
* @param {string}
|
|
60
|
+
* @param {string} workflowType
|
|
61
61
|
* @returns {string} The local filesystem path for file operations
|
|
62
62
|
*/
|
|
63
|
-
const resolveIOPath =
|
|
63
|
+
const resolveIOPath = workflowType => join( callerDir, 'logs', resolveTraceFolder( workflowType ) );
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Resolves the file path to be reported as the trace destination.
|
|
@@ -71,19 +71,17 @@ const resolveIOPath = workflowName => join( callerDir, 'logs', resolveTraceFolde
|
|
|
71
71
|
*
|
|
72
72
|
* If the env variable is not present, it falls back to the same value used to write files locally.
|
|
73
73
|
*
|
|
74
|
-
* @param {string}
|
|
74
|
+
* @param {string} workflowType - The name of the workflow
|
|
75
75
|
* @returns {string} The path to report, reflecting the actual filesystem
|
|
76
76
|
*/
|
|
77
|
-
const resolveReportPath =
|
|
78
|
-
join( process.env.OUTPUT_TRACE_HOST_PATH, resolveTraceFolder(
|
|
79
|
-
resolveIOPath(
|
|
77
|
+
const resolveReportPath = workflowType => process.env.OUTPUT_TRACE_HOST_PATH ?
|
|
78
|
+
join( process.env.OUTPUT_TRACE_HOST_PATH, resolveTraceFolder( workflowType ) ) :
|
|
79
|
+
resolveIOPath( workflowType );
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Builds the actual trace filename
|
|
83
83
|
*
|
|
84
|
-
* @param {object}
|
|
85
|
-
* @param {number} options.startTime
|
|
86
|
-
* @param {string} options.workflowId
|
|
84
|
+
* @param {object} traceInfo - The trace information object
|
|
87
85
|
* @returns {string}
|
|
88
86
|
*/
|
|
89
87
|
const buildTraceFilename = ( { startTime, workflowId } ) => {
|
|
@@ -108,15 +106,15 @@ export const init = () => {
|
|
|
108
106
|
*
|
|
109
107
|
* @param {object} args
|
|
110
108
|
* @param {object} args.entry - The trace entry to append.
|
|
111
|
-
* @param {object} args.
|
|
109
|
+
* @param {object} args.traceInfo - Trace information object
|
|
112
110
|
* @returns {void}
|
|
113
111
|
*/
|
|
114
|
-
export const exec = async ( { entry,
|
|
115
|
-
const {
|
|
116
|
-
const tempFilePath = createTempFilePath(
|
|
112
|
+
export const exec = async ( { entry, traceInfo } ) => {
|
|
113
|
+
const { runId, workflowType } = traceInfo;
|
|
114
|
+
const tempFilePath = createTempFilePath( traceInfo );
|
|
117
115
|
addEntry( entry, tempFilePath );
|
|
118
116
|
|
|
119
|
-
const isRootWorkflowEnd = entry.id ===
|
|
117
|
+
const isRootWorkflowEnd = entry.id === runId && entry.action !== 'start';
|
|
120
118
|
const isError = entry.action === 'error';
|
|
121
119
|
|
|
122
120
|
if ( !isRootWorkflowEnd && !isError ) {
|
|
@@ -124,8 +122,8 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
const content = buildTraceTree( getEntries( tempFilePath ) );
|
|
127
|
-
const dir = resolveIOPath(
|
|
128
|
-
const path = join( dir, buildTraceFilename(
|
|
125
|
+
const dir = resolveIOPath( workflowType );
|
|
126
|
+
const path = join( dir, buildTraceFilename( traceInfo ) );
|
|
129
127
|
|
|
130
128
|
mkdirSync( dir, { recursive: true } );
|
|
131
129
|
|
|
@@ -140,11 +138,8 @@ export const exec = async ( { entry, executionContext } ) => {
|
|
|
140
138
|
*
|
|
141
139
|
* This uses the optional OUTPUT_TRACE_HOST_PATH to return values relative to the host OS, not the container, if applicable.
|
|
142
140
|
*
|
|
143
|
-
* @param {object}
|
|
144
|
-
* @param {string} executionContext.startTime - The start time of the workflow
|
|
145
|
-
* @param {string} executionContext.workflowId - The id of the workflow execution
|
|
146
|
-
* @param {string} executionContext.workflowName - The name of the workflow
|
|
141
|
+
* @param {object} info
|
|
147
142
|
* @returns {string} The absolute path where the trace will be saved
|
|
148
143
|
*/
|
|
149
|
-
export const getDestination =
|
|
150
|
-
join( resolveReportPath(
|
|
144
|
+
export const getDestination = traceInfo =>
|
|
145
|
+
join( resolveReportPath( traceInfo.workflowType ), buildTraceFilename( traceInfo ) );
|
|
@@ -52,12 +52,20 @@ vi.mock( 'json-stream-stringify', async () => {
|
|
|
52
52
|
const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
|
|
53
53
|
vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
|
|
54
54
|
|
|
55
|
-
/** Flush happens when the root id matches
|
|
56
|
-
const rootStart = (
|
|
57
|
-
const rootEnd = (
|
|
55
|
+
/** Flush happens when the root id matches runId and action is not 'start', or when action is 'error'. */
|
|
56
|
+
const rootStart = ( runId, ts ) => ( { id: runId, action: 'start', timestamp: ts } );
|
|
57
|
+
const rootEnd = ( runId, ts ) => ( { id: runId, action: 'end', timestamp: ts } );
|
|
58
58
|
const childTick = ( id, ts ) => ( { id, action: 'tick', timestamp: ts } );
|
|
59
59
|
|
|
60
60
|
describe( 'tracing/processors/local', () => {
|
|
61
|
+
const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
|
|
62
|
+
const traceInfo = {
|
|
63
|
+
workflowId: 'id1',
|
|
64
|
+
runId: 'run-1',
|
|
65
|
+
workflowType: 'WF',
|
|
66
|
+
startTime
|
|
67
|
+
};
|
|
68
|
+
|
|
61
69
|
beforeEach( () => {
|
|
62
70
|
vi.clearAllMocks();
|
|
63
71
|
store.files.clear();
|
|
@@ -81,13 +89,9 @@ describe( 'tracing/processors/local', () => {
|
|
|
81
89
|
const { exec, init } = await import( './index.js' );
|
|
82
90
|
init();
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
89
|
-
await exec( { ...ctx, entry: childTick( 'child-1', startTime + 1 ) } );
|
|
90
|
-
await exec( { ...ctx, entry: rootEnd( workflowId, startTime + 2 ) } );
|
|
92
|
+
await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
|
|
93
|
+
await exec( { traceInfo, entry: childTick( 'child-1', startTime + 1 ) } );
|
|
94
|
+
await exec( { traceInfo, entry: rootEnd( traceInfo.runId, startTime + 2 ) } );
|
|
91
95
|
|
|
92
96
|
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
93
97
|
expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 3 );
|
|
@@ -103,12 +107,8 @@ describe( 'tracing/processors/local', () => {
|
|
|
103
107
|
const { exec, init } = await import( './index.js' );
|
|
104
108
|
init();
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
109
|
-
|
|
110
|
-
await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
111
|
-
await exec( { ...ctx, entry: childTick( 'child-1', startTime + 1 ) } );
|
|
110
|
+
await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
|
|
111
|
+
await exec( { traceInfo, entry: childTick( 'child-1', startTime + 1 ) } );
|
|
112
112
|
|
|
113
113
|
expect( buildTraceTreeMock ).not.toHaveBeenCalled();
|
|
114
114
|
expect( writeFileSyncMock ).not.toHaveBeenCalled();
|
|
@@ -120,12 +120,8 @@ describe( 'tracing/processors/local', () => {
|
|
|
120
120
|
const { exec, init } = await import( './index.js' );
|
|
121
121
|
init();
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
126
|
-
|
|
127
|
-
await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
128
|
-
await exec( { ...ctx, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
|
|
123
|
+
await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
|
|
124
|
+
await exec( { traceInfo, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
|
|
129
125
|
|
|
130
126
|
expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
|
|
131
127
|
expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 2 );
|
|
@@ -136,11 +132,11 @@ describe( 'tracing/processors/local', () => {
|
|
|
136
132
|
it( 'getDestination(): returns absolute path under callerDir logs', async () => {
|
|
137
133
|
const { getDestination } = await import( './index.js' );
|
|
138
134
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
135
|
+
const destination = getDestination( {
|
|
136
|
+
...traceInfo,
|
|
137
|
+
workflowId: 'workflow-id-123',
|
|
138
|
+
workflowType: 'test-workflow'
|
|
139
|
+
} );
|
|
144
140
|
|
|
145
141
|
expect( destination ).toMatch( /^\/|^[A-Z]:\\/i );
|
|
146
142
|
expect( destination ).toBe(
|
|
@@ -155,12 +151,8 @@ describe( 'tracing/processors/local', () => {
|
|
|
155
151
|
|
|
156
152
|
init();
|
|
157
153
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
|
|
161
|
-
|
|
162
|
-
await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
|
|
163
|
-
await exec( { ...ctx, entry: rootEnd( workflowId, startTime + 1 ) } );
|
|
154
|
+
await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
|
|
155
|
+
await exec( { traceInfo, entry: rootEnd( traceInfo.runId, startTime + 1 ) } );
|
|
164
156
|
|
|
165
157
|
expect( createWriteStreamMock ).toHaveBeenCalledTimes( 1 );
|
|
166
158
|
const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
|
|
@@ -174,11 +166,11 @@ describe( 'tracing/processors/local', () => {
|
|
|
174
166
|
|
|
175
167
|
process.env.OUTPUT_TRACE_HOST_PATH = '/host/path/logs';
|
|
176
168
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
169
|
+
const destination = getDestination( {
|
|
170
|
+
...traceInfo,
|
|
171
|
+
workflowId: 'workflow-id-123',
|
|
172
|
+
workflowType: 'test-workflow'
|
|
173
|
+
} );
|
|
182
174
|
|
|
183
175
|
expect( destination ).toBe( '/host/path/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
|
|
184
176
|
} );
|
|
@@ -190,15 +182,17 @@ describe( 'tracing/processors/local', () => {
|
|
|
190
182
|
|
|
191
183
|
init();
|
|
192
184
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
185
|
+
const testTraceInfo = {
|
|
186
|
+
...traceInfo,
|
|
187
|
+
workflowId: 'workflow-id-123',
|
|
188
|
+
runId: 'run-123',
|
|
189
|
+
workflowType: 'test-workflow'
|
|
190
|
+
};
|
|
197
191
|
|
|
198
|
-
await exec( {
|
|
199
|
-
await exec( {
|
|
192
|
+
await exec( { traceInfo: testTraceInfo, entry: rootStart( testTraceInfo.runId, startTime ) } );
|
|
193
|
+
await exec( { traceInfo: testTraceInfo, entry: rootEnd( testTraceInfo.runId, startTime + 1 ) } );
|
|
200
194
|
|
|
201
|
-
const destination = getDestination(
|
|
195
|
+
const destination = getDestination( testTraceInfo );
|
|
202
196
|
|
|
203
197
|
const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
|
|
204
198
|
expect( writtenPath ).not.toContain( '/Users/ben/project' );
|