@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.
Files changed (51) hide show
  1. package/package.json +2 -2
  2. package/src/activity_integration/context.d.ts +5 -9
  3. package/src/activity_integration/context.js +5 -4
  4. package/src/activity_integration/context.spec.js +10 -15
  5. package/src/activity_integration/events.d.ts +2 -4
  6. package/src/activity_integration/events.js +8 -3
  7. package/src/activity_integration/events.spec.js +58 -29
  8. package/src/bus.js +18 -9
  9. package/src/bus.spec.js +30 -0
  10. package/src/hooks/index.d.ts +112 -58
  11. package/src/hooks/index.js +15 -12
  12. package/src/hooks/index.spec.js +60 -32
  13. package/src/interface/workflow.js +19 -35
  14. package/src/interface/workflow.spec.js +104 -15
  15. package/src/internal_activities/index.js +3 -3
  16. package/src/internal_activities/index.spec.js +31 -1
  17. package/src/internal_utils/temporal_context.js +12 -0
  18. package/src/internal_utils/temporal_context.spec.ts +83 -0
  19. package/src/internal_utils/trace_info.js +21 -0
  20. package/src/internal_utils/trace_info.spec.js +47 -0
  21. package/src/internal_utils/workflow_context.js +29 -0
  22. package/src/internal_utils/workflow_context.spec.js +46 -0
  23. package/src/logger/development.js +61 -0
  24. package/src/logger/development.spec.js +70 -0
  25. package/src/logger/index.js +14 -0
  26. package/src/logger/index.spec.js +27 -0
  27. package/src/logger/production.js +15 -0
  28. package/src/logger/production.spec.js +52 -0
  29. package/src/tracing/internal_interface.js +4 -4
  30. package/src/tracing/processors/local/index.js +21 -26
  31. package/src/tracing/processors/local/index.spec.js +39 -45
  32. package/src/tracing/processors/s3/index.js +13 -23
  33. package/src/tracing/processors/s3/index.spec.js +33 -26
  34. package/src/tracing/trace_attribute.js +0 -1
  35. package/src/tracing/trace_engine.js +8 -12
  36. package/src/tracing/trace_engine.spec.js +31 -27
  37. package/src/worker/configs.js +2 -0
  38. package/src/worker/configs.spec.js +25 -0
  39. package/src/worker/index.js +4 -1
  40. package/src/worker/index.spec.js +4 -0
  41. package/src/worker/interceptors/activity.js +31 -29
  42. package/src/worker/interceptors/activity.spec.js +58 -26
  43. package/src/worker/interceptors/workflow.js +7 -2
  44. package/src/worker/interceptors/workflow.spec.js +42 -6
  45. package/src/worker/log_hooks.js +35 -46
  46. package/src/worker/log_hooks.spec.js +43 -46
  47. package/src/worker/setup_telemetry.js +19 -0
  48. package/src/worker/setup_telemetry.spec.js +80 -0
  49. package/src/worker/sinks.js +24 -24
  50. package/src/interface/workflow_context.js +0 -33
  51. package/src/logger.js +0 -73
@@ -7,7 +7,7 @@ import { JsonStreamStringify } from 'json-stream-stringify';
7
7
 
8
8
  const log = createChildLogger( 'S3 Processor' );
9
9
 
10
- const createRedisKey = ( { workflowId, workflowName } ) => `traces/${workflowName}/${workflowId}`;
10
+ const createRedisKey = runId => `traces/${runId}`;
11
11
 
12
12
  /**
13
13
  * Add new entry to list of entries
@@ -44,17 +44,14 @@ const bustEntries = async key => {
44
44
 
45
45
  /**
46
46
  * Return the S3 key for the trace file
47
- * @param {object} args
48
- * @param {number} args.startTime
49
- * @param {string} args.workflowId
50
- * @param {string} args.workflowName
47
+ * @param {object} traceInfo
51
48
  * @returns
52
49
  */
53
- const getS3Key = ( { startTime, workflowId, workflowName } ) => {
50
+ const getS3Key = ( { startTime, workflowId, workflowType } ) => {
54
51
  const isoDate = new Date( startTime ).toISOString();
55
52
  const [ year, month, day ] = isoDate.split( /\D/, 3 );
56
53
  const timeStamp = isoDate.replace( /[:T.]/g, '-' );
57
- return `${workflowName}/${year}/${month}/${day}/${timeStamp}_${workflowId}.json`;
54
+ return `${workflowType}/${year}/${month}/${day}/${timeStamp}_${workflowId}.json`;
58
55
  };
59
56
 
60
57
  /**
@@ -70,19 +67,19 @@ export const init = async () => {
70
67
  *
71
68
  * Appends each trace entry to Redis.
72
69
  *
73
- * When the root workflow ends or the entry is an error action, it builds the trace tree and uploads it to S3.
70
+ * When the root workflow finishes or errors, builds the trace tree and uploads it to S3.
74
71
  *
75
72
  * @param {object} args
76
73
  * @param {object} args.entry - The trace entry to append
77
- * @param {object} args.executionContext - Execution info: workflowId, workflowName, startTime
74
+ * @param {object} args.traceInfo - Trace information object
78
75
  */
79
- export const exec = async ( { entry, executionContext } ) => {
80
- const { workflowName, workflowId, startTime } = executionContext;
81
- const cacheKey = createRedisKey( { workflowId, workflowName } );
76
+ export const exec = async ( { entry, traceInfo } ) => {
77
+ const { workflowId, runId } = traceInfo;
78
+ const cacheKey = createRedisKey( runId );
82
79
 
83
80
  await addEntry( entry, cacheKey );
84
81
 
85
- const isRootWorkflowEnd = entry.id === workflowId && entry.action !== 'start';
82
+ const isRootWorkflowEnd = entry.id === runId && entry.action !== 'start';
86
83
  if ( !isRootWorkflowEnd ) {
87
84
  return;
88
85
  }
@@ -100,20 +97,13 @@ export const exec = async ( { entry, executionContext } ) => {
100
97
  return;
101
98
  }
102
99
 
103
- await upload( {
104
- key: getS3Key( { workflowId, workflowName, startTime } ),
105
- content: new JsonStreamStringify( content )
106
- } );
100
+ await upload( { key: getS3Key( traceInfo ), content: new JsonStreamStringify( content ) } );
107
101
  await bustEntries( cacheKey );
108
102
  };
109
103
 
110
104
  /**
111
105
  * Returns where the trace is saved
112
- * @param {object} executionContext
113
- * @param {string} executionContext.startTime - The start time of the workflow
114
- * @param {string} executionContext.workflowId - The id of the workflow execution
115
- * @param {string} executionContext.workflowName - The name of the workflow
106
+ * @param {object} traceInfo - Trace information object
116
107
  * @returns {string} The S3 url of the trace file
117
108
  */
118
- export const getDestination = ( { startTime, workflowId, workflowName } ) =>
119
- `https://${getVars().remoteS3Bucket}.s3.amazonaws.com/${getS3Key( { workflowId, workflowName, startTime } )}`;
109
+ export const getDestination = traceInfo => `https://${getVars().remoteS3Bucket}.s3.amazonaws.com/${getS3Key( traceInfo )}`;
@@ -53,6 +53,14 @@ const streamToString = async stream => {
53
53
  };
54
54
 
55
55
  describe( 'tracing/processors/s3', () => {
56
+ const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
57
+ const traceInfo = {
58
+ workflowId: 'id1',
59
+ runId: 'run-1',
60
+ workflowType: 'WF',
61
+ startTime
62
+ };
63
+
56
64
  beforeEach( () => {
57
65
  vi.useFakeTimers();
58
66
  vi.clearAllMocks();
@@ -72,24 +80,30 @@ describe( 'tracing/processors/s3', () => {
72
80
 
73
81
  it( 'exec(): accumulates via redis, uploads only on root workflow end', async () => {
74
82
  const { exec } = await import( './index.js' );
75
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
76
- const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
77
83
 
78
84
  redisMulti.exec.mockResolvedValue( [] );
79
85
 
80
- const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
81
- const activityStart = { id: 'act-1', name: 'DoSomething', kind: 'step', parentId: 'id1', action: 'start', details: {}, timestamp: startTime + 1 };
82
- const workflowEnd = { id: 'id1', action: 'end', details: { ok: true }, timestamp: startTime + 2 };
86
+ const workflowStart = { id: 'run-1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
87
+ const activityStart = {
88
+ id: 'act-1',
89
+ name: 'DoSomething',
90
+ kind: 'step',
91
+ parentId: 'run-1',
92
+ action: 'start',
93
+ details: {},
94
+ timestamp: startTime + 1
95
+ };
96
+ const workflowEnd = { id: 'run-1', action: 'end', details: { ok: true }, timestamp: startTime + 2 };
83
97
  zRangeMock.mockResolvedValue( [
84
98
  JSON.stringify( workflowStart ),
85
99
  JSON.stringify( activityStart ),
86
100
  JSON.stringify( workflowEnd )
87
101
  ] );
88
102
 
89
- await exec( { ...ctx, entry: workflowStart } );
90
- await exec( { ...ctx, entry: activityStart } );
91
- // Root end: id matches workflowId and not start — triggers the 10s delay before upload
92
- const endPromise = exec( { ...ctx, entry: workflowEnd } );
103
+ await exec( { traceInfo, entry: workflowStart } );
104
+ await exec( { traceInfo, entry: activityStart } );
105
+ // Root end: id matches runId and not start — triggers the 10s delay before upload
106
+ const endPromise = exec( { traceInfo, entry: workflowEnd } );
93
107
  await vi.advanceTimersByTimeAsync( 10_000 );
94
108
  await endPromise;
95
109
 
@@ -101,14 +115,13 @@ describe( 'tracing/processors/s3', () => {
101
115
  expect( key ).toMatch( /^WF\/2020\/01\/02\// );
102
116
  expect( JSON.parse( await streamToString( content ) ).count ).toBe( 3 );
103
117
  expect( delMock ).toHaveBeenCalledTimes( 1 );
104
- expect( delMock ).toHaveBeenCalledWith( 'traces/WF/id1' );
118
+ expect( delMock ).toHaveBeenCalledWith( 'traces/run-1' );
105
119
  } );
106
120
 
107
121
  it( 'getDestination(): returns S3 URL using bucket and key from getVars', async () => {
108
122
  getVarsMock.mockReturnValue( { remoteS3Bucket: 'my-bucket', redisIncompleteWorkflowsTTL: 3600, traceUploadDelayMs: 10_000 } );
109
123
  const { getDestination } = await import( './index.js' );
110
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
111
- const url = getDestination( { workflowId: 'id1', workflowName: 'WF', startTime } );
124
+ const url = getDestination( traceInfo );
112
125
  expect( getVarsMock ).toHaveBeenCalled();
113
126
  expect( url ).toBe(
114
127
  'https://my-bucket.s3.amazonaws.com/WF/2020/01/02/2020-01-02-03-04-05-678Z_id1.json'
@@ -117,36 +130,32 @@ describe( 'tracing/processors/s3', () => {
117
130
 
118
131
  it( 'exec(): sets expiry on the redis key for each entry', async () => {
119
132
  const { exec } = await import( './index.js' );
120
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
121
- const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
122
133
 
123
134
  redisMulti.exec.mockResolvedValue( [] );
124
135
  const workflowStart = {
125
- kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, action: 'start', details: {}, timestamp: startTime
136
+ kind: 'workflow', id: 'run-1', name: 'WF', parentId: undefined, action: 'start', details: {}, timestamp: startTime
126
137
  };
127
138
  zRangeMock.mockResolvedValue( [ JSON.stringify( workflowStart ) ] );
128
139
 
129
- await exec( { ...ctx, entry: workflowStart } );
140
+ await exec( { traceInfo, entry: workflowStart } );
130
141
 
131
142
  expect( redisMulti.expire ).toHaveBeenCalledTimes( 1 );
132
- expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/WF/id1', 3600 );
143
+ expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/run-1', 3600 );
133
144
  } );
134
145
 
135
146
  it( 'exec(): does not treat a non-root end (e.g. step without parentId) as root workflow end — regression for wrong root detection', async () => {
136
147
  const { exec } = await import( './index.js' );
137
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
138
- const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
139
148
 
140
149
  redisMulti.exec.mockResolvedValue( [] );
141
- const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
150
+ const workflowStart = { id: 'run-1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
142
151
  const stepEndNoParent = { id: 'step-1', action: 'end', details: { done: true }, timestamp: startTime + 1 };
143
152
  zRangeMock.mockResolvedValue( [
144
153
  JSON.stringify( workflowStart ),
145
154
  JSON.stringify( stepEndNoParent )
146
155
  ] );
147
156
 
148
- await exec( { ...ctx, entry: workflowStart } );
149
- await exec( { ...ctx, entry: stepEndNoParent } );
157
+ await exec( { traceInfo, entry: workflowStart } );
158
+ await exec( { traceInfo, entry: stepEndNoParent } );
150
159
 
151
160
  expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 2 );
152
161
  expect( buildTraceTreeMock ).not.toHaveBeenCalled();
@@ -156,17 +165,15 @@ describe( 'tracing/processors/s3', () => {
156
165
 
157
166
  it( 'exec(): when buildTraceTree returns null (incomplete tree), does not upload or bust cache', async () => {
158
167
  const { exec } = await import( './index.js' );
159
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
160
- const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
161
168
 
162
169
  redisMulti.exec.mockResolvedValue( [] );
163
170
  const workflowEnd = {
164
- kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, action: 'end', details: {}, timestamp: startTime
171
+ kind: 'workflow', id: 'run-1', name: 'WF', parentId: undefined, action: 'end', details: {}, timestamp: startTime
165
172
  };
166
173
  zRangeMock.mockResolvedValue( [ JSON.stringify( workflowEnd ) ] );
167
174
  buildTraceTreeMock.mockReturnValueOnce( null );
168
175
 
169
- const endPromise = exec( { ...ctx, entry: workflowEnd } );
176
+ const endPromise = exec( { traceInfo, entry: workflowEnd } );
170
177
  await vi.advanceTimersByTimeAsync( 10_000 );
171
178
  await endPromise;
172
179
 
@@ -4,7 +4,6 @@ import Decimal from 'decimal.js';
4
4
  * All attributes inherit from this
5
5
  */
6
6
  export class BaseAttribute {
7
- date = Date.now();
8
7
  type;
9
8
 
10
9
  constructor( type ) {
@@ -32,16 +32,12 @@ const processors = [
32
32
  /**
33
33
  * Returns the destinations for a given execution context
34
34
  *
35
- * @param {object} executionContext
36
- * @param {string} executionContext.startTime
37
- * @param {string} executionContext.workflowId
38
- * @param {string} executionContext.workflowName
39
- * @param {boolean} executionContext.disableTrace
35
+ * @param {object} traceInfo - The trace information object
40
36
  * @returns {object} A trace destinations object: { [dest-name]: 'path' }
41
37
  */
42
- export const getDestinations = executionContext =>
38
+ export const getDestinations = traceInfo =>
43
39
  processors.reduce( ( o, p ) =>
44
- Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !executionContext.disableTrace ? p.getDestination( executionContext ) : null } )
40
+ Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !traceInfo.disableTrace ? p.getDestination( traceInfo ) : null } )
45
41
  , {} );
46
42
 
47
43
  /**
@@ -72,11 +68,11 @@ const serializeDetails = details => details instanceof Error ? serializeError( d
72
68
  * @param {object} fields - All the trace fields
73
69
  * @returns {void}
74
70
  */
75
- export const addEventAction = ( action, { kind, name, id, parentId, details, executionContext } ) => {
71
+ export const addEventAction = ( action, { kind, name, id, parentId, details, traceInfo } ) => {
76
72
  // Ignores internal steps in the actual trace files, ignore trace if the flag is true
77
- if ( kind !== ComponentType.INTERNAL_STEP && !executionContext.disableTrace ) {
73
+ if ( kind !== ComponentType.INTERNAL_STEP && !traceInfo.disableTrace ) {
78
74
  traceBus.emit( 'entry', {
79
- executionContext,
75
+ traceInfo,
80
76
  entry: { kind, action, name, id, parentId, timestamp: Date.now(), details: serializeDetails( details ) }
81
77
  } );
82
78
  }
@@ -93,7 +89,7 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, exe
93
89
  export function addEventActionWithContext( action, options ) {
94
90
  const storeContent = Storage.load();
95
91
  if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
96
- const { parentId, executionContext, addAttribute } = storeContent;
92
+ const { parentId, traceInfo, addAttribute } = storeContent;
97
93
  if ( action === EventAction.ADD_ATTR ) {
98
94
  const attribute = options.details;
99
95
  if ( !( attribute instanceof BaseAttribute ) ) {
@@ -102,6 +98,6 @@ export function addEventActionWithContext( action, options ) {
102
98
  addAttribute( options.details );
103
99
  }
104
100
  }
105
- addEventAction( action, { ...options, parentId, executionContext } );
101
+ addEventAction( action, { ...options, parentId, traceInfo } );
106
102
  }
107
103
  };
@@ -34,6 +34,14 @@ async function loadTraceEngine() {
34
34
  return import( './trace_engine.js' );
35
35
  }
36
36
 
37
+ const traceInfo = {
38
+ workflowId: 'w1',
39
+ runId: 'r1',
40
+ workflowType: 'WF',
41
+ startTime: 1,
42
+ disableTrace: false
43
+ };
44
+
37
45
  describe( 'tracing/trace_engine', () => {
38
46
  beforeEach( () => {
39
47
  vi.clearAllMocks();
@@ -52,9 +60,8 @@ describe( 'tracing/trace_engine', () => {
52
60
  expect( localInitMock ).toHaveBeenCalledTimes( 1 );
53
61
  expect( s3InitMock ).not.toHaveBeenCalled();
54
62
 
55
- const executionContext = { disableTrace: false };
56
63
  addEventAction( 'start', {
57
- kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, executionContext
64
+ kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, traceInfo
58
65
  } );
59
66
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
60
67
  const payload = localExecMock.mock.calls[0][0];
@@ -62,7 +69,7 @@ describe( 'tracing/trace_engine', () => {
62
69
  expect( payload.entry.kind ).toBe( 'step' );
63
70
  expect( payload.entry.action ).toBe( 'start' );
64
71
  expect( payload.entry.details ).toEqual( { ok: true } );
65
- expect( payload.executionContext ).toBe( executionContext );
72
+ expect( payload.traceInfo ).toBe( traceInfo );
66
73
  } );
67
74
 
68
75
  it( 'addEventAction() emits an entry consumed by processors', async () => {
@@ -72,7 +79,7 @@ describe( 'tracing/trace_engine', () => {
72
79
 
73
80
  addEventAction( 'end', {
74
81
  kind: 'workflow', name: 'W', id: '2', parentId: 'p2', details: 'done',
75
- executionContext: { disableTrace: false }
82
+ traceInfo
76
83
  } );
77
84
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
78
85
  const payload = localExecMock.mock.calls[0][0];
@@ -81,14 +88,14 @@ describe( 'tracing/trace_engine', () => {
81
88
  expect( payload.entry.details ).toBe( 'done' );
82
89
  } );
83
90
 
84
- it( 'addEventAction() does not emit when executionContext.disableTrace is true', async () => {
91
+ it( 'addEventAction() does not emit when traceInfo.disableTrace is true', async () => {
85
92
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
86
93
  const { init, addEventAction } = await loadTraceEngine();
87
94
  await init();
88
95
 
89
96
  addEventAction( 'start', {
90
97
  kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
91
- executionContext: { disableTrace: true }
98
+ traceInfo: { ...traceInfo, disableTrace: true }
92
99
  } );
93
100
  expect( localExecMock ).not.toHaveBeenCalled();
94
101
  } );
@@ -100,7 +107,7 @@ describe( 'tracing/trace_engine', () => {
100
107
 
101
108
  addEventAction( 'start', {
102
109
  kind: 'internal_step', name: 'Internal', id: '1', parentId: 'p', details: {},
103
- executionContext: { disableTrace: false }
110
+ traceInfo
104
111
  } );
105
112
  expect( localExecMock ).not.toHaveBeenCalled();
106
113
  } );
@@ -109,7 +116,7 @@ describe( 'tracing/trace_engine', () => {
109
116
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
110
117
  storageLoadMock.mockReturnValue( {
111
118
  parentId: 'ctx-p',
112
- executionContext: { runId: 'r1', disableTrace: false }
119
+ traceInfo
113
120
  } );
114
121
  const { init, addEventActionWithContext } = await loadTraceEngine();
115
122
  await init();
@@ -117,7 +124,7 @@ describe( 'tracing/trace_engine', () => {
117
124
  addEventActionWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
118
125
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
119
126
  const payload = localExecMock.mock.calls[0][0];
120
- expect( payload.executionContext ).toEqual( { runId: 'r1', disableTrace: false } );
127
+ expect( payload.traceInfo ).toBe( traceInfo );
121
128
  expect( payload.entry.parentId ).toBe( 'ctx-p' );
122
129
  expect( payload.entry.name ).toBe( 'S' );
123
130
  expect( payload.entry.action ).toBe( 'tick' );
@@ -126,10 +133,9 @@ describe( 'tracing/trace_engine', () => {
126
133
  it( 'addEventActionWithContext() records ADD_ATTR attributes through storage context', async () => {
127
134
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
128
135
  const addAttributeMock = vi.fn();
129
- const executionContext = { runId: 'r1', disableTrace: false };
130
136
  storageLoadMock.mockReturnValue( {
131
137
  parentId: 'ctx-p',
132
- executionContext,
138
+ traceInfo,
133
139
  addAttribute: addAttributeMock
134
140
  } );
135
141
  const { init, addEventActionWithContext } = await loadTraceEngine();
@@ -144,7 +150,7 @@ describe( 'tracing/trace_engine', () => {
144
150
  expect( addAttributeMock ).toHaveBeenCalledWith( attribute );
145
151
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
146
152
  expect( localExecMock.mock.calls[0][0] ).toEqual( {
147
- executionContext,
153
+ traceInfo,
148
154
  entry: {
149
155
  kind: 'http',
150
156
  action: EventAction.ADD_ATTR,
@@ -162,7 +168,7 @@ describe( 'tracing/trace_engine', () => {
162
168
  const addAttributeMock = vi.fn();
163
169
  storageLoadMock.mockReturnValue( {
164
170
  parentId: 'ctx-p',
165
- executionContext: { runId: 'r1', disableTrace: false },
171
+ traceInfo,
166
172
  addAttribute: addAttributeMock
167
173
  } );
168
174
  const { init, addEventActionWithContext } = await loadTraceEngine();
@@ -179,11 +185,11 @@ describe( 'tracing/trace_engine', () => {
179
185
  expect( localExecMock ).not.toHaveBeenCalled();
180
186
  } );
181
187
 
182
- it( 'addEventActionWithContext() does not emit when storage executionContext.disableTrace is true', async () => {
188
+ it( 'addEventActionWithContext() does not emit when storage traceInfo.disableTrace is true', async () => {
183
189
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
184
190
  storageLoadMock.mockReturnValue( {
185
191
  parentId: 'ctx-p',
186
- executionContext: { runId: 'r1', disableTrace: true }
192
+ traceInfo: { ...traceInfo, disableTrace: true }
187
193
  } );
188
194
  const { init, addEventActionWithContext } = await loadTraceEngine();
189
195
  await init();
@@ -203,21 +209,19 @@ describe( 'tracing/trace_engine', () => {
203
209
  } );
204
210
 
205
211
  describe( 'getDestinations()', () => {
206
- const executionContext = { workflowId: 'w1', workflowName: 'WF', startTime: 1, disableTrace: false };
207
-
208
212
  it( 'returns null for both when traces are off (env vars unset)', async () => {
209
213
  const { getDestinations } = await loadTraceEngine();
210
- const result = getDestinations( executionContext );
214
+ const result = getDestinations( traceInfo );
211
215
  expect( result ).toEqual( { local: null, remote: null } );
212
216
  expect( localGetDestinationMock ).not.toHaveBeenCalled();
213
217
  expect( s3GetDestinationMock ).not.toHaveBeenCalled();
214
218
  } );
215
219
 
216
- it( 'returns null for both when executionContext.disableTrace is true', async () => {
220
+ it( 'returns null for both when traceInfo.disableTrace is true', async () => {
217
221
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
218
222
  process.env.OUTPUT_TRACE_REMOTE_ON = '1';
219
223
  const { getDestinations } = await loadTraceEngine();
220
- const result = getDestinations( { ...executionContext, disableTrace: true } );
224
+ const result = getDestinations( { ...traceInfo, disableTrace: true } );
221
225
  expect( result ).toEqual( { local: null, remote: null } );
222
226
  expect( localGetDestinationMock ).not.toHaveBeenCalled();
223
227
  expect( s3GetDestinationMock ).not.toHaveBeenCalled();
@@ -227,24 +231,24 @@ describe( 'tracing/trace_engine', () => {
227
231
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
228
232
  process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
229
233
  const { getDestinations } = await loadTraceEngine();
230
- const result = getDestinations( executionContext );
234
+ const result = getDestinations( traceInfo );
231
235
  expect( result ).toEqual( {
232
236
  local: '/local/path.json',
233
237
  remote: 'https://bucket.s3.amazonaws.com/key.json'
234
238
  } );
235
239
  expect( localGetDestinationMock ).toHaveBeenCalledTimes( 1 );
236
- expect( localGetDestinationMock ).toHaveBeenCalledWith( executionContext );
240
+ expect( localGetDestinationMock ).toHaveBeenCalledWith( traceInfo );
237
241
  expect( s3GetDestinationMock ).toHaveBeenCalledTimes( 1 );
238
- expect( s3GetDestinationMock ).toHaveBeenCalledWith( executionContext );
242
+ expect( s3GetDestinationMock ).toHaveBeenCalledWith( traceInfo );
239
243
  } );
240
244
 
241
245
  it( 'returns local only when local trace on and remote off', async () => {
242
246
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
243
247
  process.env.OUTPUT_TRACE_REMOTE_ON = '0';
244
248
  const { getDestinations } = await loadTraceEngine();
245
- const result = getDestinations( executionContext );
249
+ const result = getDestinations( traceInfo );
246
250
  expect( result ).toEqual( { local: '/local/path.json', remote: null } );
247
- expect( localGetDestinationMock ).toHaveBeenCalledWith( executionContext );
251
+ expect( localGetDestinationMock ).toHaveBeenCalledWith( traceInfo );
248
252
  expect( s3GetDestinationMock ).not.toHaveBeenCalled();
249
253
  } );
250
254
 
@@ -252,10 +256,10 @@ describe( 'tracing/trace_engine', () => {
252
256
  process.env.OUTPUT_TRACE_LOCAL_ON = '0';
253
257
  process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
254
258
  const { getDestinations } = await loadTraceEngine();
255
- const result = getDestinations( executionContext );
259
+ const result = getDestinations( traceInfo );
256
260
  expect( result ).toEqual( { local: null, remote: 'https://bucket.s3.amazonaws.com/key.json' } );
257
261
  expect( localGetDestinationMock ).not.toHaveBeenCalled();
258
- expect( s3GetDestinationMock ).toHaveBeenCalledWith( executionContext );
262
+ expect( s3GetDestinationMock ).toHaveBeenCalledWith( traceInfo );
259
263
  } );
260
264
  } );
261
265
  } );
@@ -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,