@outputai/core 0.8.2-next.42a0ddf.0 → 0.8.2-next.ad732b1.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.
Files changed (56) hide show
  1. package/package.json +8 -4
  2. package/src/consts.js +5 -2
  3. package/src/interface/evaluator.js +1 -1
  4. package/src/interface/index.d.ts +7 -6
  5. package/src/interface/index.js +2 -0
  6. package/src/interface/logger.d.ts +53 -0
  7. package/src/interface/logger.js +68 -0
  8. package/src/interface/logger.spec.js +138 -0
  9. package/src/interface/step.js +1 -1
  10. package/src/interface/workflow.js +2 -1
  11. package/src/internal_activities/index.js +2 -1
  12. package/src/internal_utils/component.js +9 -0
  13. package/src/logger/development.js +1 -1
  14. package/src/logger/development.spec.js +18 -1
  15. package/src/logger/production.js +1 -1
  16. package/src/logger/production.spec.js +24 -5
  17. package/src/sdk/README.md +47 -0
  18. package/src/sdk/helpers/component_metadata.d.ts +17 -0
  19. package/src/sdk/helpers/component_metadata.js +6 -0
  20. package/src/sdk/helpers/component_metadata.spec.js +30 -0
  21. package/src/sdk/helpers/index.d.ts +11 -0
  22. package/src/sdk/helpers/index.js +2 -0
  23. package/src/sdk/helpers/path.d.ts +11 -0
  24. package/src/sdk/helpers/path.js +32 -0
  25. package/src/{utils/resolve_invocation_dir.spec.js → sdk/helpers/path.spec.js} +9 -9
  26. package/src/sdk/runtime/context.d.ts +30 -0
  27. package/src/sdk/runtime/context.js +15 -0
  28. package/src/{activity_integration → sdk/runtime}/context.spec.js +5 -5
  29. package/src/sdk/runtime/events.d.ts +15 -0
  30. package/src/sdk/runtime/events.js +18 -0
  31. package/src/{activity_integration → sdk/runtime}/events.spec.js +8 -9
  32. package/src/sdk/runtime/index.d.ts +12 -0
  33. package/src/sdk/runtime/index.js +3 -0
  34. package/src/sdk/runtime/tracing.d.ts +46 -0
  35. package/src/sdk/runtime/tracing.js +11 -0
  36. package/src/utils/index.d.ts +0 -32
  37. package/src/utils/index.js +0 -1
  38. package/src/utils/utils.js +0 -27
  39. package/src/worker/global_functions.js +20 -0
  40. package/src/worker/global_functions.spec.js +55 -0
  41. package/src/worker/index.js +3 -0
  42. package/src/worker/index.spec.js +7 -0
  43. package/src/worker/log_hooks.js +14 -0
  44. package/src/worker/log_hooks.spec.js +83 -2
  45. package/src/worker/sinks.js +6 -0
  46. package/src/worker/sinks.spec.js +203 -0
  47. package/src/activity_integration/context.d.ts +0 -23
  48. package/src/activity_integration/context.js +0 -18
  49. package/src/activity_integration/event_id_integration.spec.js +0 -52
  50. package/src/activity_integration/events.d.ts +0 -10
  51. package/src/activity_integration/events.js +0 -15
  52. package/src/activity_integration/index.d.ts +0 -9
  53. package/src/activity_integration/index.js +0 -3
  54. package/src/activity_integration/tracing.d.ts +0 -40
  55. package/src/activity_integration/tracing.js +0 -48
  56. package/src/utils/resolve_invocation_dir.js +0 -34
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
- import resolveInvocationDir from './resolve_invocation_dir';
2
+ import { Path } from './path.js';
3
3
 
4
4
  const OriginalError = Error;
5
5
 
@@ -10,7 +10,7 @@ describe( 'Resolve Invocation Dir', () => {
10
10
 
11
11
  it( 'Should detect the invocation dir from the tests workflow', () => {
12
12
  const stack = `Error
13
- at resolveInvocationDir (file:///app/sdk/core/src/utils/resolve_invocation_dir.js)
13
+ at resolveInvocationDir (file:///app/sdk/core/src/sdk/helpers/path.js)
14
14
  at fn (file:///app/test_workflows/dist/simple/steps.js:8:21)
15
15
  at wrapper (file:///app/sdk/core/src/interface/step.js:12:26)
16
16
  at executeNextHandler (/app/node_modules/@temporalio/worker/lib/activity.js:99:54)
@@ -29,12 +29,12 @@ describe( 'Resolve Invocation Dir', () => {
29
29
  }
30
30
  };
31
31
 
32
- expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/simple' );
32
+ expect( Path.resolveInvocationDir() ).toBe( '/app/test_workflows/dist/simple' );
33
33
  } );
34
34
 
35
35
  it( 'Should detect the invocation dir from the sandbox environment at sdk/core', () => {
36
36
  const stack = `Error
37
- at resolveInvocationDir (file:///app/sdk/core/src/utils/resolve_invocation_dir.js)
37
+ at resolveInvocationDir (file:///app/sdk/core/src/sdk/helpers/path.js)
38
38
  at workflow (file:///app/sdk/core/src/interface/workflow.js:25:16)
39
39
  at file:///app/test_workflows/dist/nested/workflow.js:4:16
40
40
  at ModuleJob.run (node:internal/modules/esm/module_job:365:25)
@@ -50,12 +50,12 @@ describe( 'Resolve Invocation Dir', () => {
50
50
  }
51
51
  };
52
52
 
53
- expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
53
+ expect( Path.resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
54
54
  } );
55
55
 
56
56
  it( 'Should detect the invocation dir from workflow loading at core', () => {
57
57
  const stack = `Error
58
- at __WEBPACK_DEFAULT_EXPORT__ (/app/sdk/core/src/utils/resolve_invocation_dir.js:13:0)
58
+ at resolveInvocationDir (/app/sdk/core/src/sdk/helpers/path.js:21:0)
59
59
  at workflow (/app/sdk/core/src/interface/workflow.js:22:43)
60
60
  at ../../test_workflows/dist/nested/workflow.js (/app/test_workflows/dist/nested/workflow.js:4:24)
61
61
  at __webpack_require__ (webpack/bootstrap:19:0)
@@ -73,12 +73,12 @@ describe( 'Resolve Invocation Dir', () => {
73
73
  }
74
74
  };
75
75
 
76
- expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
76
+ expect( Path.resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
77
77
  } );
78
78
 
79
79
  it( 'Should detect the invocation dir from a workflow using core installed via NPM', () => {
80
80
  const stack = `Error
81
- at resolveInvocationDir (file:///app/node_modules/@outputai/core/src/utils/resolve_invocation_dir.js)
81
+ at resolveInvocationDir (file:///app/node_modules/@outputai/core/src/sdk/helpers/path.js)
82
82
  at fn (file:///app/dist/simple/steps.js:8:21)
83
83
  at wrapper (file:///app/node_modules/@outputai/core/src/interface/step.js:12:26)
84
84
  at executeNextHandler (/app/node_modules/@temporalio/worker/lib/activity.js:99:54)
@@ -97,6 +97,6 @@ describe( 'Resolve Invocation Dir', () => {
97
97
  }
98
98
  };
99
99
 
100
- expect( resolveInvocationDir() ).toBe( '/app/dist/simple' );
100
+ expect( Path.resolveInvocationDir() ).toBe( '/app/dist/simple' );
101
101
  } );
102
102
  } );
@@ -0,0 +1,30 @@
1
+ import type { Info } from '@temporalio/activity';
2
+
3
+ /**
4
+ * Context object
5
+ */
6
+ export type Context = {
7
+ /** Temporal info about the current activity */
8
+ activityInfo: Info,
9
+ /** Path of the workflow file */
10
+ workflowFilename: string
11
+ };
12
+
13
+ /**
14
+ * Tools to interact with Runtime context
15
+ */
16
+ export declare const Context: {
17
+
18
+ /**
19
+ * Returns information about the current Temporal execution.
20
+ *
21
+ * Only available when called from within a step or evaluator (Temporal Activities) running in the Temporal runtime.
22
+ *
23
+ * @remarks
24
+ * - Returns `null` when not called inside a Temporal Activity (steps/evaluators);
25
+ * - Returns `null` when not called from within a running Temporal worker, like in unit tests environment;
26
+ *
27
+ * @returns The workflow context, or `null` if unavailable or incomplete.
28
+ */
29
+ getActivityContext(): Context | null;
30
+ };
@@ -0,0 +1,15 @@
1
+ import { Storage } from '#async_storage';
2
+
3
+ export const Context = {
4
+ getActivityContext: () => {
5
+ const ctx = Storage.load();
6
+ if ( !ctx ) {
7
+ return null;
8
+ }
9
+
10
+ return {
11
+ workflowFilename: ctx.workflowFilename,
12
+ activityInfo: ctx.activityInfo
13
+ };
14
+ }
15
+ };
@@ -5,7 +5,7 @@ vi.mock( '#async_storage', () => ( {
5
5
  Storage: { load: loadMock }
6
6
  } ) );
7
7
 
8
- describe( 'getExecutionContext', () => {
8
+ describe( 'Context.getActivityContext', () => {
9
9
  beforeEach( () => {
10
10
  vi.clearAllMocks();
11
11
  vi.resetModules();
@@ -13,8 +13,8 @@ describe( 'getExecutionContext', () => {
13
13
 
14
14
  it( 'returns null when no context is stored', async () => {
15
15
  loadMock.mockReturnValue( undefined );
16
- const { getExecutionContext } = await import( './index.js' );
17
- expect( getExecutionContext() ).toBeNull();
16
+ const { Context } = await import( './index.js' );
17
+ expect( Context.getActivityContext() ).toBeNull();
18
18
  } );
19
19
 
20
20
  it( 'returns activity execution context from storage', async () => {
@@ -28,8 +28,8 @@ describe( 'getExecutionContext', () => {
28
28
  activityInfo,
29
29
  workflowFilename: '/workflows/myWorkflow.js'
30
30
  } );
31
- const { getExecutionContext } = await import( './index.js' );
32
- expect( getExecutionContext() ).toEqual( {
31
+ const { Context } = await import( './index.js' );
32
+ expect( Context.getActivityContext() ).toEqual( {
33
33
  activityInfo,
34
34
  workflowFilename: '/workflows/myWorkflow.js'
35
35
  } );
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tools to interact with Events
3
+ */
4
+ export declare const Event: {
5
+ /**
6
+ * Emits a custom event on the in-process message bus.
7
+ *
8
+ * When called inside an Output activity context, the framework automatically
9
+ * attaches `activityInfo`, `workflowDetails`, and `outputActivityKind` onto the emitted payload.
10
+ *
11
+ * @param eventName - The name of the event to emit
12
+ * @param payload - An optional payload to send to the event
13
+ */
14
+ emit( eventName: string, payload?: object ): void;
15
+ };
@@ -0,0 +1,18 @@
1
+ import { messageBus } from '#bus';
2
+ import { Storage } from '#async_storage';
3
+
4
+ export const Event = {
5
+ emit: ( eventName, payload ) => {
6
+ const ctx = Storage.load();
7
+
8
+ messageBus.emit( `external:${eventName}`, {
9
+ ...payload ?? {},
10
+ ...( ctx && {
11
+ activityInfo: ctx.activityInfo,
12
+ workflowDetails: ctx.workflowDetails,
13
+ outputActivityKind: ctx.outputActivityKind
14
+ } )
15
+ } );
16
+ }
17
+ };
18
+
@@ -11,12 +11,11 @@ vi.mock( '#bus', () => ( {
11
11
  messageBus: { emit: emitMock }
12
12
  } ) );
13
13
 
14
- import { emitEvent } from './events.js';
14
+ import { Event } from './events.js';
15
15
 
16
- // `eventId` stamping is the bus layer's responsibility (see bus.spec.js + the
17
- // integration tests in event_id_integration.spec.js). Assertions here use
18
- // `objectContaining` so they don't have to know about that enrichment.
19
- describe( 'emitEvent', () => {
16
+ // `eventId` stamping is the bus layer's responsibility (see bus.spec.js).
17
+ // Assertions here use `objectContaining` so they don't have to know about that enrichment.
18
+ describe( 'Event.emit', () => {
20
19
  beforeEach( () => {
21
20
  vi.clearAllMocks();
22
21
  } );
@@ -39,7 +38,7 @@ describe( 'emitEvent', () => {
39
38
  outputActivityKind: 'step'
40
39
  } );
41
40
 
42
- emitEvent( 'cost:llm:request', { modelId: 'gpt-4o' } );
41
+ Event.emit( 'cost:llm:request', { modelId: 'gpt-4o' } );
43
42
 
44
43
  expect( emitMock ).toHaveBeenCalledWith( 'external:cost:llm:request', expect.objectContaining( {
45
44
  activityInfo,
@@ -52,7 +51,7 @@ describe( 'emitEvent', () => {
52
51
  it( 'emits payload without context when storage is missing', () => {
53
52
  loadMock.mockReturnValue( undefined );
54
53
 
55
- emitEvent( 'foo:bar', { x: 1 } );
54
+ Event.emit( 'foo:bar', { x: 1 } );
56
55
 
57
56
  expect( emitMock ).toHaveBeenCalledWith( 'external:foo:bar', { x: 1 } );
58
57
  } );
@@ -75,7 +74,7 @@ describe( 'emitEvent', () => {
75
74
  outputActivityKind: 'step'
76
75
  } );
77
76
 
78
- emitEvent( 'lifecycle:start' );
77
+ Event.emit( 'lifecycle:start' );
79
78
 
80
79
  expect( emitMock ).toHaveBeenCalledWith( 'external:lifecycle:start', expect.objectContaining( {
81
80
  activityInfo,
@@ -102,7 +101,7 @@ describe( 'emitEvent', () => {
102
101
  outputActivityKind: 'step'
103
102
  } );
104
103
 
105
- emitEvent( 'cost:http:request', {
104
+ Event.emit( 'cost:http:request', {
106
105
  activityInfo: { activityId: 'should-be-overridden' },
107
106
  workflowDetails: { workflowId: 'should-be-overridden' },
108
107
  outputActivityKind: 'should-be-overridden',
@@ -0,0 +1,12 @@
1
+ /**
2
+ * > [!WARNING]
3
+ * > **Internal use only.** Not part of the public API; may change without notice.
4
+ *
5
+ * These are helpers for other SDK modules integration.
6
+ * These need Temporal activity runtime to work, as they access state, emit events and use node:* modules.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export * from './context.js';
11
+ export * from './events.js';
12
+ export * from './tracing.js';
@@ -0,0 +1,3 @@
1
+ export * from './context.js';
2
+ export * from './events.js';
3
+ export * from './tracing.js';
@@ -0,0 +1,46 @@
1
+ import { Attribute } from '#trace_attribute';
2
+
3
+ /**
4
+ * Tools to interact with Tracing
5
+ */
6
+ export declare const Tracing: {
7
+ Attribute: typeof Attribute;
8
+
9
+ /**
10
+ * Creates a new event.
11
+ *
12
+ * @param args
13
+ * @param args.id - A unique id for the Event.
14
+ * @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
15
+ * @param args.name - The human-friendly name of the Event: query, request, create.
16
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
17
+ */
18
+ addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
19
+
20
+ /**
21
+ * Concludes an event.
22
+ *
23
+ * @param args
24
+ * @param args.id - The id of the event to conclude.
25
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
26
+ */
27
+ addEventEnd( args: { id: string; details: unknown } ): void;
28
+
29
+ /**
30
+ * Concludes an event with an error.
31
+ *
32
+ * @param args
33
+ * @param args.id - The id of the event to conclude.
34
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
35
+ */
36
+ addEventError( args: { id: string; details: unknown } ): void;
37
+
38
+ /**
39
+ * Adds an attribute to an event.
40
+ *
41
+ * @param args
42
+ * @param args.eventId - The id of the event to attach the attribute to.
43
+ * @param args.attribute - The attribute to attach to the event.
44
+ */
45
+ addEventAttribute( args: { eventId: string; attribute: Attribute.Instance } ): void;
46
+ };
@@ -0,0 +1,11 @@
1
+ import { addEventActionWithContext as send, EventAction } from '#tracing';
2
+
3
+ import { Attribute } from '#trace_attribute';
4
+
5
+ export const Tracing = {
6
+ Attribute,
7
+ addEventStart: ( { id, kind, name, details } ) => send( EventAction.START, { kind, name, details, id } ),
8
+ addEventEnd: ( { id, details } ) => send( EventAction.END, { id, details } ),
9
+ addEventError: ( { id, details } ) => send( EventAction.ERROR, { id, details } ),
10
+ addEventAttribute: ( { eventId, attribute } ) => send( EventAction.ADD_ATTR, { id: eventId, details: attribute } )
11
+ };
@@ -5,13 +5,6 @@
5
5
  * @packageDocumentation
6
6
  */
7
7
 
8
- /**
9
- * Return the first immediate directory of the file invoking the code that called this function.
10
- *
11
- * Excludes `@outputai/core`, node, and other internal paths.
12
- */
13
- export function resolveInvocationDir(): string;
14
-
15
8
  /**
16
9
  * Node safe clone implementation that doesn't use global structuredClone().
17
10
  *
@@ -23,31 +16,6 @@ export function resolveInvocationDir(): string;
23
16
  */
24
17
  export function clone( object: object ): object;
25
18
 
26
- /**
27
- * Receives an error as argument and throws it.
28
- *
29
- * @param error
30
- * @throws {Error}
31
- */
32
- export function throws( error: Error ): void;
33
-
34
- /**
35
- * Attach given value to an object with the METADATA_ACCESS_SYMBOL symbol as key.
36
- *
37
- * @param target
38
- * @param value
39
- * @returns
40
- */
41
- export function setMetadata( target: object, value: object ): void;
42
-
43
- /**
44
- * Read metadata previously attached via setMetadata.
45
- *
46
- * @param target - The function or object to read metadata from.
47
- * @returns The metadata object, or null if none is attached.
48
- */
49
- export function getMetadata( target: Function ): { name: string; description?: string; type?: string } | null;
50
-
51
19
  /** Represents a {Response} serialized to plain object */
52
20
  export type SerializedFetchResponse = {
53
21
  /** The response url */
@@ -1,2 +1 @@
1
- export { default as resolveInvocationDir } from './resolve_invocation_dir.js';
2
1
  export * from './utils.js';
@@ -1,5 +1,3 @@
1
- import { METADATA_ACCESS_SYMBOL } from '#consts';
2
-
3
1
  /**
4
2
  * Node safe clone implementation that doesn't use global structuredClone()
5
3
  * @param {object} v
@@ -25,31 +23,6 @@ export const isPlainObject = v =>
25
23
  v !== null &&
26
24
  [ Object.prototype, null ].includes( Object.getPrototypeOf( v ) );
27
25
 
28
- /**
29
- * Throw given error
30
- * @param {Error} e
31
- * @throws {e}
32
- */
33
- export const throws = e => {
34
- throw e;
35
- };
36
-
37
- /**
38
- * Add metadata "values" property to a given object
39
- * @param {object} target
40
- * @param {object} values
41
- * @returns
42
- */
43
- export const setMetadata = ( target, values ) =>
44
- Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
45
-
46
- /**
47
- * Read metadata previously attached via setMetadata
48
- * @param {Function} target
49
- * @returns {object|null}
50
- */
51
- export const getMetadata = target => target[METADATA_ACCESS_SYMBOL] ?? null;
52
-
53
26
  /**
54
27
  * Returns true if string value is stringbool and true
55
28
  * @param {string} v
@@ -0,0 +1,20 @@
1
+ import { messageBus } from '#bus';
2
+ import { ACTIVITY_LOGGER_SYMBOL, BusEventType } from '#consts';
3
+ import { activityInfo as activityInfoFn } from '@temporalio/activity';
4
+
5
+ const setFunction = ( key, fn ) => Object.defineProperty( globalThis, key, {
6
+ value: fn,
7
+ enumerable: false,
8
+ configurable: false,
9
+ writable: false
10
+ } );
11
+
12
+ /**
13
+ * Sets global functions on globalThis
14
+ */
15
+ export const bindGlobalFunctions = () => {
16
+ /** Defines the activity logger function, accessible in activity context via logger interface */
17
+ setFunction( ACTIVITY_LOGGER_SYMBOL, ( { level, message, metadata } ) =>
18
+ messageBus.emit( BusEventType.ACTIVITY_LOG, { level, message, metadata, activityInfo: activityInfoFn() } )
19
+ );
20
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const {
4
+ ACTIVITY_LOGGER_SYMBOL,
5
+ BusEventType
6
+ } = vi.hoisted( () => ( {
7
+ ACTIVITY_LOGGER_SYMBOL: Symbol( 'activity_logger' ),
8
+ BusEventType: {
9
+ ACTIVITY_LOG: 'activity:log'
10
+ }
11
+ } ) );
12
+
13
+ const messageBusMock = vi.hoisted( () => ( {
14
+ emit: vi.fn()
15
+ } ) );
16
+ const activityInfoMock = vi.hoisted( () => vi.fn( () => ( {
17
+ activityId: 'act-1',
18
+ activityType: 'myStep'
19
+ } ) ) );
20
+
21
+ vi.mock( '#consts', () => ( { ACTIVITY_LOGGER_SYMBOL, BusEventType } ) );
22
+ vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
23
+ vi.mock( '@temporalio/activity', () => ( {
24
+ activityInfo: activityInfoMock
25
+ } ) );
26
+
27
+ describe( 'worker/global_functions', () => {
28
+ beforeEach( () => {
29
+ vi.clearAllMocks();
30
+ delete globalThis[ACTIVITY_LOGGER_SYMBOL];
31
+ } );
32
+
33
+ it( 'binds activity logger global function that emits activity log events with metadata', async () => {
34
+ const { bindGlobalFunctions } = await import( './global_functions.js' );
35
+ const metadata = { requestId: 'req-1' };
36
+
37
+ bindGlobalFunctions();
38
+ globalThis[ACTIVITY_LOGGER_SYMBOL]( {
39
+ level: 'debug',
40
+ message: 'activity detail',
41
+ metadata
42
+ } );
43
+
44
+ expect( activityInfoMock ).toHaveBeenCalledTimes( 1 );
45
+ expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.ACTIVITY_LOG, {
46
+ level: 'debug',
47
+ message: 'activity detail',
48
+ metadata,
49
+ activityInfo: {
50
+ activityId: 'act-1',
51
+ activityType: 'myStep'
52
+ }
53
+ } );
54
+ } );
55
+ } );
@@ -17,6 +17,7 @@ import { messageBus } from '#bus';
17
17
  import { BusEventType } from '#consts';
18
18
  import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
20
+ import { bindGlobalFunctions } from './global_functions.js';
20
21
  import { runOnce } from '#utils';
21
22
 
22
23
  import './log_hooks.js';
@@ -76,6 +77,8 @@ const execute = async () => {
76
77
  log.info( 'Using gRPC proxy', { targetHost: grpcProxy } );
77
78
  }
78
79
 
80
+ bindGlobalFunctions();
81
+
79
82
  state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
80
83
 
81
84
  log.info( 'Creating connection monitor...' );
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  const {
4
4
  catalogJobInstance,
5
+ bindGlobalFunctionsMock,
5
6
  configValues,
6
7
  connectionMonitorInstance,
7
8
  createCatalogMock,
@@ -98,6 +99,7 @@ const {
98
99
 
99
100
  return {
100
101
  catalogJobInstance,
102
+ bindGlobalFunctionsMock: vi.fn(),
101
103
  configValues,
102
104
  connectionMonitorInstance,
103
105
  createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
@@ -145,6 +147,7 @@ vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptors
145
147
  vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
146
148
  vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
147
149
  vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
150
+ vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
148
151
  vi.mock( './connection_monitor.js', () => ( {
149
152
  TemporalConnectionMonitor: vi.fn( function () {
150
153
  return connectionMonitorInstance;
@@ -222,6 +225,10 @@ describe( 'worker/index', () => {
222
225
  apiKey: undefined,
223
226
  proxy: undefined
224
227
  } );
228
+ expect( bindGlobalFunctionsMock ).toHaveBeenCalledTimes( 1 );
229
+ expect( bindGlobalFunctionsMock.mock.invocationCallOrder[0] ).toBeLessThan(
230
+ NativeConnection.connect.mock.invocationCallOrder[0]
231
+ );
225
232
  expect( TemporalConnectionMonitor ).toHaveBeenCalledWith( mockConnection );
226
233
  expect( CatalogJob ).toHaveBeenCalledWith( {
227
234
  connection: mockConnection,
@@ -47,6 +47,13 @@ messageBus.on( BusEventType.ACTIVITY_ERROR, ( { activityInfo, outputActivityKind
47
47
  } )
48
48
  );
49
49
 
50
+ messageBus.on( BusEventType.ACTIVITY_LOG, ( { level, message, metadata, activityInfo } ) =>
51
+ activityLog[level]( message, {
52
+ ...metadata ?? {},
53
+ ...serializedActivityFields( activityInfo )
54
+ } )
55
+ );
56
+
50
57
  /*
51
58
  ╔═════════════════╗
52
59
  ║ Workflow events ║
@@ -82,3 +89,10 @@ messageBus.on( BusEventType.WORKFLOW_ERROR, ( { workflowDetails, error } ) =>
82
89
  error: error.message
83
90
  } )
84
91
  );
92
+
93
+ messageBus.on( BusEventType.WORKFLOW_LOG, ( { level, message, metadata, workflowDetails } ) =>
94
+ workflowLog[level]( message, {
95
+ ...metadata ?? {},
96
+ ...serializeWorkflowFields( workflowDetails )
97
+ } )
98
+ );
@@ -6,8 +6,17 @@ import {
6
6
  WORKFLOW_CATALOG
7
7
  } from '#consts';
8
8
 
9
- const activityLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
10
- const workflowLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
9
+ const createLoggerMock = vi.hoisted( () => () => ( {
10
+ error: vi.fn(),
11
+ warn: vi.fn(),
12
+ info: vi.fn(),
13
+ http: vi.fn(),
14
+ verbose: vi.fn(),
15
+ debug: vi.fn(),
16
+ silly: vi.fn()
17
+ } ) );
18
+ const activityLogMock = vi.hoisted( () => createLoggerMock() );
19
+ const workflowLogMock = vi.hoisted( () => createLoggerMock() );
11
20
  const createChildLoggerMock = vi.hoisted( () =>
12
21
  vi.fn( name => ( name === 'Activity' ? activityLogMock : workflowLogMock ) )
13
22
  );
@@ -125,6 +134,44 @@ describe( 'log_hooks', () => {
125
134
 
126
135
  expect( activityLogMock.error ).not.toHaveBeenCalled();
127
136
  } );
137
+
138
+ it( 'ACTIVITY_LOG logs dynamic levels with metadata and serialized activity fields', () => {
139
+ onHandlers[BusEventType.ACTIVITY_LOG]( {
140
+ activityInfo,
141
+ level: 'debug',
142
+ message: 'activity detail',
143
+ metadata: {
144
+ custom: 'value',
145
+ workflowId: 'metadata-workflow-id'
146
+ }
147
+ } );
148
+
149
+ expect( activityLogMock.debug ).toHaveBeenCalledTimes( 1 );
150
+ expect( activityLogMock.debug ).toHaveBeenCalledWith( 'activity detail', {
151
+ custom: 'value',
152
+ activityId: 'act-1',
153
+ activityType: 'myWorkflow#myStep',
154
+ workflowId: 'wf-1',
155
+ workflowType: 'myWorkflow',
156
+ runId: 'run-1'
157
+ } );
158
+ } );
159
+
160
+ it( 'ACTIVITY_LOG accepts omitted metadata', () => {
161
+ onHandlers[BusEventType.ACTIVITY_LOG]( {
162
+ activityInfo,
163
+ level: 'info',
164
+ message: 'activity detail'
165
+ } );
166
+
167
+ expect( activityLogMock.info ).toHaveBeenCalledWith( 'activity detail', {
168
+ activityId: 'act-1',
169
+ activityType: 'myWorkflow#myStep',
170
+ workflowId: 'wf-1',
171
+ workflowType: 'myWorkflow',
172
+ runId: 'run-1'
173
+ } );
174
+ } );
128
175
  } );
129
176
 
130
177
  describe( 'workflow events', () => {
@@ -210,5 +257,39 @@ describe( 'log_hooks', () => {
210
257
 
211
258
  expect( workflowLogMock.error ).not.toHaveBeenCalled();
212
259
  } );
260
+
261
+ it( 'WORKFLOW_LOG logs dynamic levels with metadata and serialized workflow fields', () => {
262
+ onHandlers[BusEventType.WORKFLOW_LOG]( {
263
+ workflowDetails,
264
+ level: 'warn',
265
+ message: 'workflow detail',
266
+ metadata: {
267
+ custom: 'value',
268
+ runId: 'metadata-run-id'
269
+ }
270
+ } );
271
+
272
+ expect( workflowLogMock.warn ).toHaveBeenCalledTimes( 1 );
273
+ expect( workflowLogMock.warn ).toHaveBeenCalledWith( 'workflow detail', {
274
+ custom: 'value',
275
+ workflowId: 'wf-1',
276
+ workflowType: 'myWorkflow',
277
+ runId: 'run-1'
278
+ } );
279
+ } );
280
+
281
+ it( 'WORKFLOW_LOG accepts omitted metadata', () => {
282
+ onHandlers[BusEventType.WORKFLOW_LOG]( {
283
+ workflowDetails,
284
+ level: 'info',
285
+ message: 'workflow detail'
286
+ } );
287
+
288
+ expect( workflowLogMock.info ).toHaveBeenCalledWith( 'workflow detail', {
289
+ workflowId: 'wf-1',
290
+ workflowType: 'myWorkflow',
291
+ runId: 'run-1'
292
+ } );
293
+ } );
213
294
  } );
214
295
  } );
@@ -10,6 +10,12 @@ export const sinks = {
10
10
  * Workflow lifecycle sinks
11
11
  */
12
12
  workflow: {
13
+ log: {
14
+ fn: ( workflowInfo, { level, message, metadata } ) => {
15
+ messageBus.emit( BusEventType.WORKFLOW_LOG, { level, message, metadata, workflowDetails: createWorkflowDetails( workflowInfo ) } );
16
+ },
17
+ callDuringReplay: false
18
+ },
13
19
  start: {
14
20
  fn: ( workflowInfo, input ) => {
15
21
  const { runId, workflowType, memo: { traceInfo }, parent } = workflowInfo;