@braintrust/temporal 0.1.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/README.md +54 -0
- package/dist/chunk-KT66NY2E.mjs +39 -0
- package/dist/index.d.mts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +301 -0
- package/dist/index.mjs +257 -0
- package/dist/workflow-interceptors.d.mts +13 -0
- package/dist/workflow-interceptors.d.ts +13 -0
- package/dist/workflow-interceptors.js +134 -0
- package/dist/workflow-interceptors.mjs +95 -0
- package/examples/temporal/.env.example +2 -0
- package/examples/temporal/Procfile +4 -0
- package/examples/temporal/README.md +183 -0
- package/examples/temporal/mise.toml +40 -0
- package/examples/temporal/package.json +26 -0
- package/examples/temporal/src/activities.ts +42 -0
- package/examples/temporal/src/client.ts +53 -0
- package/examples/temporal/src/worker.ts +31 -0
- package/examples/temporal/src/workflows.ts +47 -0
- package/examples/temporal/tsconfig.json +15 -0
- package/package.json +71 -0
- package/src/index.ts +50 -0
- package/src/interceptors.ts +184 -0
- package/src/plugin.ts +128 -0
- package/src/sinks.ts +76 -0
- package/src/temporal.test.ts +243 -0
- package/src/utils.ts +29 -0
- package/src/workflow-interceptors.ts +159 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +20 -0
- package/turbo.json +8 -0
- package/vitest.config.ts +5 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRAINTRUST_SPAN_HEADER,
|
|
3
|
+
BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
|
|
4
|
+
__require,
|
|
5
|
+
deserializeHeaderValue
|
|
6
|
+
} from "./chunk-KT66NY2E.mjs";
|
|
7
|
+
|
|
8
|
+
// src/interceptors.ts
|
|
9
|
+
import { defaultPayloadConverter } from "@temporalio/common";
|
|
10
|
+
import * as braintrust2 from "braintrust";
|
|
11
|
+
import { SpanComponentsV3 } from "braintrust/util";
|
|
12
|
+
|
|
13
|
+
// src/sinks.ts
|
|
14
|
+
import * as braintrust from "braintrust";
|
|
15
|
+
var workflowSpans = /* @__PURE__ */ new Map();
|
|
16
|
+
var workflowSpanExports = /* @__PURE__ */ new Map();
|
|
17
|
+
function getWorkflowSpanExport(runId) {
|
|
18
|
+
return workflowSpanExports.get(runId);
|
|
19
|
+
}
|
|
20
|
+
function createBraintrustSinks() {
|
|
21
|
+
return {
|
|
22
|
+
braintrust: {
|
|
23
|
+
workflowStarted: {
|
|
24
|
+
fn: (info, parentContext, workflowSpanId) => {
|
|
25
|
+
const span = braintrust.startSpan({
|
|
26
|
+
name: `temporal.workflow.${info.workflowType}`,
|
|
27
|
+
spanAttributes: { type: "task" },
|
|
28
|
+
parent: parentContext,
|
|
29
|
+
spanId: workflowSpanId,
|
|
30
|
+
event: {
|
|
31
|
+
metadata: {
|
|
32
|
+
"temporal.workflow_type": info.workflowType,
|
|
33
|
+
"temporal.workflow_id": info.workflowId,
|
|
34
|
+
"temporal.run_id": info.runId
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
workflowSpans.set(info.runId, span);
|
|
39
|
+
workflowSpanExports.set(info.runId, span.export());
|
|
40
|
+
},
|
|
41
|
+
callDuringReplay: false
|
|
42
|
+
},
|
|
43
|
+
workflowCompleted: {
|
|
44
|
+
fn: (info, error) => {
|
|
45
|
+
const span = workflowSpans.get(info.runId);
|
|
46
|
+
if (span) {
|
|
47
|
+
if (error) {
|
|
48
|
+
span.log({ error });
|
|
49
|
+
}
|
|
50
|
+
span.end();
|
|
51
|
+
workflowSpans.delete(info.runId);
|
|
52
|
+
workflowSpanExports.delete(info.runId);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
callDuringReplay: false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/interceptors.ts
|
|
62
|
+
function createBraintrustClientInterceptor() {
|
|
63
|
+
return {
|
|
64
|
+
async start(input, next) {
|
|
65
|
+
const span = braintrust2.currentSpan();
|
|
66
|
+
if (span) {
|
|
67
|
+
const exported = await span.export();
|
|
68
|
+
if (exported) {
|
|
69
|
+
const payload = defaultPayloadConverter.toPayload(exported);
|
|
70
|
+
if (payload) {
|
|
71
|
+
return next({
|
|
72
|
+
...input,
|
|
73
|
+
headers: {
|
|
74
|
+
...input.headers,
|
|
75
|
+
[BRAINTRUST_SPAN_HEADER]: payload
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return next(input);
|
|
82
|
+
},
|
|
83
|
+
async signal(input, next) {
|
|
84
|
+
return next(input);
|
|
85
|
+
},
|
|
86
|
+
async signalWithStart(input, next) {
|
|
87
|
+
const span = braintrust2.currentSpan();
|
|
88
|
+
if (span) {
|
|
89
|
+
const exported = await span.export();
|
|
90
|
+
if (exported) {
|
|
91
|
+
const payload = defaultPayloadConverter.toPayload(exported);
|
|
92
|
+
if (payload) {
|
|
93
|
+
return next({
|
|
94
|
+
...input,
|
|
95
|
+
headers: {
|
|
96
|
+
...input.headers,
|
|
97
|
+
[BRAINTRUST_SPAN_HEADER]: payload
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return next(input);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var BraintrustActivityInterceptor = class {
|
|
108
|
+
constructor(ctx) {
|
|
109
|
+
this.ctx = ctx;
|
|
110
|
+
}
|
|
111
|
+
async execute(input, next) {
|
|
112
|
+
const info = this.ctx.info;
|
|
113
|
+
const runId = info.workflowExecution.runId;
|
|
114
|
+
let parent;
|
|
115
|
+
const spanExportPromise = getWorkflowSpanExport(runId);
|
|
116
|
+
if (spanExportPromise) {
|
|
117
|
+
try {
|
|
118
|
+
parent = await spanExportPromise;
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!parent && input.headers) {
|
|
123
|
+
const workflowSpanId = deserializeHeaderValue(
|
|
124
|
+
input.headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER]
|
|
125
|
+
);
|
|
126
|
+
const clientContext = deserializeHeaderValue(
|
|
127
|
+
input.headers[BRAINTRUST_SPAN_HEADER]
|
|
128
|
+
);
|
|
129
|
+
if (workflowSpanId && clientContext) {
|
|
130
|
+
try {
|
|
131
|
+
const clientComponents = SpanComponentsV3.fromStr(clientContext);
|
|
132
|
+
const clientData = clientComponents.data;
|
|
133
|
+
const hasTracingContext = !!clientData.root_span_id;
|
|
134
|
+
const hasObjectMetadata = !!clientData.object_id || !!clientData.compute_object_metadata_args;
|
|
135
|
+
if (hasTracingContext && hasObjectMetadata) {
|
|
136
|
+
const workflowComponents = new SpanComponentsV3({
|
|
137
|
+
object_type: clientData.object_type,
|
|
138
|
+
object_id: clientData.object_id || void 0,
|
|
139
|
+
compute_object_metadata_args: clientData.object_id ? void 0 : clientData.compute_object_metadata_args || void 0,
|
|
140
|
+
propagated_event: clientData.propagated_event,
|
|
141
|
+
row_id: workflowSpanId,
|
|
142
|
+
// Use workflow's row_id, not client's
|
|
143
|
+
span_id: workflowSpanId,
|
|
144
|
+
// Use workflow's span_id, not client's
|
|
145
|
+
root_span_id: clientData.root_span_id
|
|
146
|
+
// Keep same trace
|
|
147
|
+
});
|
|
148
|
+
parent = workflowComponents.toStr();
|
|
149
|
+
} else {
|
|
150
|
+
parent = clientContext;
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
parent = clientContext;
|
|
154
|
+
}
|
|
155
|
+
} else if (clientContext) {
|
|
156
|
+
parent = clientContext;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const span = braintrust2.startSpan({
|
|
160
|
+
name: `temporal.activity.${info.activityType}`,
|
|
161
|
+
spanAttributes: { type: "task" },
|
|
162
|
+
parent,
|
|
163
|
+
event: {
|
|
164
|
+
metadata: {
|
|
165
|
+
"temporal.activity_type": info.activityType,
|
|
166
|
+
"temporal.activity_id": info.activityId,
|
|
167
|
+
"temporal.workflow_id": info.workflowExecution.workflowId,
|
|
168
|
+
"temporal.workflow_run_id": runId
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
const result = await braintrust2.withCurrent(span, () => next(input));
|
|
174
|
+
span.log({ output: result });
|
|
175
|
+
span.end();
|
|
176
|
+
return result;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
span.log({ error: String(e) });
|
|
179
|
+
span.end();
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function createBraintrustActivityInterceptor(ctx) {
|
|
185
|
+
return {
|
|
186
|
+
inbound: new BraintrustActivityInterceptor(ctx)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/plugin.ts
|
|
191
|
+
var BraintrustTemporalPlugin = class {
|
|
192
|
+
get name() {
|
|
193
|
+
return "braintrust";
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Configure the Temporal Client with Braintrust interceptors.
|
|
197
|
+
* Adds the client interceptor for propagating span context to workflows.
|
|
198
|
+
*/
|
|
199
|
+
configureClient(options) {
|
|
200
|
+
const existing = options.interceptors?.workflow;
|
|
201
|
+
const braintrustInterceptor = createBraintrustClientInterceptor();
|
|
202
|
+
let workflow;
|
|
203
|
+
if (Array.isArray(existing)) {
|
|
204
|
+
workflow = [...existing, braintrustInterceptor];
|
|
205
|
+
} else if (existing) {
|
|
206
|
+
workflow = {
|
|
207
|
+
...existing,
|
|
208
|
+
...braintrustInterceptor
|
|
209
|
+
};
|
|
210
|
+
} else {
|
|
211
|
+
workflow = [braintrustInterceptor];
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
...options,
|
|
215
|
+
interceptors: {
|
|
216
|
+
...options.interceptors,
|
|
217
|
+
workflow
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Configure the Temporal Worker with Braintrust interceptors and sinks.
|
|
223
|
+
* Adds the activity interceptor for creating spans, the sinks for workflow spans,
|
|
224
|
+
* and the workflow interceptor modules for bundling.
|
|
225
|
+
*/
|
|
226
|
+
configureWorker(options) {
|
|
227
|
+
const existingActivityInterceptors = options.interceptors?.activity ?? [];
|
|
228
|
+
const existingWorkflowModules = options.interceptors?.workflowModules ?? [];
|
|
229
|
+
const existingSinks = options.sinks ?? {};
|
|
230
|
+
const braintrustSinks = createBraintrustSinks();
|
|
231
|
+
const workflowInterceptorsPath = __require.resolve(
|
|
232
|
+
"@braintrust/temporal/workflow-interceptors"
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
...options,
|
|
236
|
+
interceptors: {
|
|
237
|
+
...options.interceptors,
|
|
238
|
+
activity: [
|
|
239
|
+
...existingActivityInterceptors,
|
|
240
|
+
createBraintrustActivityInterceptor
|
|
241
|
+
],
|
|
242
|
+
workflowModules: [...existingWorkflowModules, workflowInterceptorsPath]
|
|
243
|
+
},
|
|
244
|
+
sinks: {
|
|
245
|
+
...existingSinks,
|
|
246
|
+
...braintrustSinks
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
function createBraintrustTemporalPlugin() {
|
|
252
|
+
return new BraintrustTemporalPlugin();
|
|
253
|
+
}
|
|
254
|
+
export {
|
|
255
|
+
BraintrustTemporalPlugin,
|
|
256
|
+
createBraintrustTemporalPlugin
|
|
257
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WorkflowInterceptorsFactory } from '@temporalio/workflow';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow interceptors for Braintrust tracing.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: This module is loaded into the Temporal workflow isolate.
|
|
7
|
+
* It cannot import Node.js modules or access external state directly.
|
|
8
|
+
* Communication with the outside world is done via sinks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare const interceptors: WorkflowInterceptorsFactory;
|
|
12
|
+
|
|
13
|
+
export { interceptors };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WorkflowInterceptorsFactory } from '@temporalio/workflow';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow interceptors for Braintrust tracing.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: This module is loaded into the Temporal workflow isolate.
|
|
7
|
+
* It cannot import Node.js modules or access external state directly.
|
|
8
|
+
* Communication with the outside world is done via sinks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare const interceptors: WorkflowInterceptorsFactory;
|
|
12
|
+
|
|
13
|
+
export { interceptors };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/workflow-interceptors.ts
|
|
21
|
+
var workflow_interceptors_exports = {};
|
|
22
|
+
__export(workflow_interceptors_exports, {
|
|
23
|
+
interceptors: () => interceptors
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(workflow_interceptors_exports);
|
|
26
|
+
var import_workflow = require("@temporalio/workflow");
|
|
27
|
+
|
|
28
|
+
// src/utils.ts
|
|
29
|
+
var BRAINTRUST_SPAN_HEADER = "_braintrust-span";
|
|
30
|
+
var BRAINTRUST_WORKFLOW_SPAN_HEADER = "_braintrust-workflow-span";
|
|
31
|
+
var BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = "_braintrust-workflow-span-id";
|
|
32
|
+
function serializeHeaderValue(value) {
|
|
33
|
+
return {
|
|
34
|
+
metadata: {
|
|
35
|
+
encoding: new TextEncoder().encode("json/plain")
|
|
36
|
+
},
|
|
37
|
+
data: new TextEncoder().encode(JSON.stringify(value))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function deserializeHeaderValue(payload) {
|
|
41
|
+
if (!payload?.data) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const decoded = new TextDecoder().decode(payload.data);
|
|
46
|
+
return JSON.parse(decoded);
|
|
47
|
+
} catch {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/workflow-interceptors.ts
|
|
53
|
+
var { braintrust } = (0, import_workflow.proxySinks)();
|
|
54
|
+
var BraintrustWorkflowInboundInterceptor = class {
|
|
55
|
+
constructor(state) {
|
|
56
|
+
this.state = state;
|
|
57
|
+
}
|
|
58
|
+
async execute(input, next) {
|
|
59
|
+
const parentContext = input.headers ? deserializeHeaderValue(input.headers[BRAINTRUST_SPAN_HEADER]) : void 0;
|
|
60
|
+
this.state.parentContext = parentContext;
|
|
61
|
+
this.state.spanId = (0, import_workflow.uuid4)();
|
|
62
|
+
braintrust.workflowStarted(parentContext, this.state.spanId);
|
|
63
|
+
try {
|
|
64
|
+
const result = await next(input);
|
|
65
|
+
braintrust.workflowCompleted();
|
|
66
|
+
return result;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
braintrust.workflowCompleted(e instanceof Error ? e.message : String(e));
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var BraintrustWorkflowOutboundInterceptor = class {
|
|
74
|
+
constructor(state) {
|
|
75
|
+
this.state = state;
|
|
76
|
+
}
|
|
77
|
+
getHeaders() {
|
|
78
|
+
const info = (0, import_workflow.workflowInfo)();
|
|
79
|
+
const headers = {};
|
|
80
|
+
headers[BRAINTRUST_WORKFLOW_SPAN_HEADER] = serializeHeaderValue(info.runId);
|
|
81
|
+
if (this.state.spanId) {
|
|
82
|
+
headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER] = serializeHeaderValue(
|
|
83
|
+
this.state.spanId
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (this.state.parentContext) {
|
|
87
|
+
headers[BRAINTRUST_SPAN_HEADER] = serializeHeaderValue(
|
|
88
|
+
this.state.parentContext
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return headers;
|
|
92
|
+
}
|
|
93
|
+
scheduleActivity(input, next) {
|
|
94
|
+
return next({
|
|
95
|
+
...input,
|
|
96
|
+
headers: {
|
|
97
|
+
...input.headers,
|
|
98
|
+
...this.getHeaders()
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
scheduleLocalActivity(input, next) {
|
|
103
|
+
return next({
|
|
104
|
+
...input,
|
|
105
|
+
headers: {
|
|
106
|
+
...input.headers,
|
|
107
|
+
...this.getHeaders()
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
startChildWorkflowExecution(input, next) {
|
|
112
|
+
return next({
|
|
113
|
+
...input,
|
|
114
|
+
headers: {
|
|
115
|
+
...input.headers,
|
|
116
|
+
...this.getHeaders()
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var interceptors = () => {
|
|
122
|
+
const state = {
|
|
123
|
+
parentContext: void 0,
|
|
124
|
+
spanId: void 0
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
inbound: [new BraintrustWorkflowInboundInterceptor(state)],
|
|
128
|
+
outbound: [new BraintrustWorkflowOutboundInterceptor(state)]
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
132
|
+
0 && (module.exports = {
|
|
133
|
+
interceptors
|
|
134
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRAINTRUST_SPAN_HEADER,
|
|
3
|
+
BRAINTRUST_WORKFLOW_SPAN_HEADER,
|
|
4
|
+
BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
|
|
5
|
+
deserializeHeaderValue,
|
|
6
|
+
serializeHeaderValue
|
|
7
|
+
} from "./chunk-KT66NY2E.mjs";
|
|
8
|
+
|
|
9
|
+
// src/workflow-interceptors.ts
|
|
10
|
+
import {
|
|
11
|
+
proxySinks,
|
|
12
|
+
workflowInfo,
|
|
13
|
+
uuid4
|
|
14
|
+
} from "@temporalio/workflow";
|
|
15
|
+
var { braintrust } = proxySinks();
|
|
16
|
+
var BraintrustWorkflowInboundInterceptor = class {
|
|
17
|
+
constructor(state) {
|
|
18
|
+
this.state = state;
|
|
19
|
+
}
|
|
20
|
+
async execute(input, next) {
|
|
21
|
+
const parentContext = input.headers ? deserializeHeaderValue(input.headers[BRAINTRUST_SPAN_HEADER]) : void 0;
|
|
22
|
+
this.state.parentContext = parentContext;
|
|
23
|
+
this.state.spanId = uuid4();
|
|
24
|
+
braintrust.workflowStarted(parentContext, this.state.spanId);
|
|
25
|
+
try {
|
|
26
|
+
const result = await next(input);
|
|
27
|
+
braintrust.workflowCompleted();
|
|
28
|
+
return result;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
braintrust.workflowCompleted(e instanceof Error ? e.message : String(e));
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var BraintrustWorkflowOutboundInterceptor = class {
|
|
36
|
+
constructor(state) {
|
|
37
|
+
this.state = state;
|
|
38
|
+
}
|
|
39
|
+
getHeaders() {
|
|
40
|
+
const info = workflowInfo();
|
|
41
|
+
const headers = {};
|
|
42
|
+
headers[BRAINTRUST_WORKFLOW_SPAN_HEADER] = serializeHeaderValue(info.runId);
|
|
43
|
+
if (this.state.spanId) {
|
|
44
|
+
headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER] = serializeHeaderValue(
|
|
45
|
+
this.state.spanId
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (this.state.parentContext) {
|
|
49
|
+
headers[BRAINTRUST_SPAN_HEADER] = serializeHeaderValue(
|
|
50
|
+
this.state.parentContext
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return headers;
|
|
54
|
+
}
|
|
55
|
+
scheduleActivity(input, next) {
|
|
56
|
+
return next({
|
|
57
|
+
...input,
|
|
58
|
+
headers: {
|
|
59
|
+
...input.headers,
|
|
60
|
+
...this.getHeaders()
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
scheduleLocalActivity(input, next) {
|
|
65
|
+
return next({
|
|
66
|
+
...input,
|
|
67
|
+
headers: {
|
|
68
|
+
...input.headers,
|
|
69
|
+
...this.getHeaders()
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
startChildWorkflowExecution(input, next) {
|
|
74
|
+
return next({
|
|
75
|
+
...input,
|
|
76
|
+
headers: {
|
|
77
|
+
...input.headers,
|
|
78
|
+
...this.getHeaders()
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var interceptors = () => {
|
|
84
|
+
const state = {
|
|
85
|
+
parentContext: void 0,
|
|
86
|
+
spanId: void 0
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
inbound: [new BraintrustWorkflowInboundInterceptor(state)],
|
|
90
|
+
outbound: [new BraintrustWorkflowOutboundInterceptor(state)]
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
export {
|
|
94
|
+
interceptors
|
|
95
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Temporal + Braintrust Tracing Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates how to integrate Braintrust tracing with Temporal workflows and activities.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [mise](https://mise.jdx.dev/) (recommended) - automatically installs all dependencies
|
|
8
|
+
- OR manually install:
|
|
9
|
+
- Node.js 20+
|
|
10
|
+
- `pnpm`
|
|
11
|
+
- Temporal CLI (`temporal`)
|
|
12
|
+
- Optional: [`overmind`](https://github.com/DarthSim/overmind) (only if you want to use the included `Procfile`)
|
|
13
|
+
|
|
14
|
+
### Option 1: Using mise (recommended)
|
|
15
|
+
|
|
16
|
+
[mise](https://mise.jdx.dev/) will automatically install and manage all required tools (Node.js, Temporal CLI, overmind, and dependencies):
|
|
17
|
+
|
|
18
|
+
**Install mise:**
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# macOS/Linux
|
|
22
|
+
curl https://mise.run | sh
|
|
23
|
+
|
|
24
|
+
# Or using Homebrew
|
|
25
|
+
brew install mise
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Setup and run:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Copy and configure environment
|
|
32
|
+
cp .env.example .env
|
|
33
|
+
# Edit .env with your BRAINTRUST_API_KEY
|
|
34
|
+
|
|
35
|
+
# mise will automatically install tools and dependencies
|
|
36
|
+
mise run server # Start temporal server and workers
|
|
37
|
+
|
|
38
|
+
# In another terminal:
|
|
39
|
+
mise run workflow # Run the workflow client
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Available mise tasks:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
mise run install # Install dependencies
|
|
46
|
+
mise run server # Run temporal server and workers
|
|
47
|
+
mise run workflow # Run workflow client
|
|
48
|
+
mise run stop # Stop temporal server and workers
|
|
49
|
+
mise run kill # Force kill all processes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Option 2: Manual installation
|
|
53
|
+
|
|
54
|
+
#### Installing Temporal CLI
|
|
55
|
+
|
|
56
|
+
The Temporal CLI is required to run the local Temporal server:
|
|
57
|
+
|
|
58
|
+
**macOS:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
brew install temporal
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Linux:**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Using Homebrew
|
|
68
|
+
brew install temporal
|
|
69
|
+
|
|
70
|
+
# Or using curl
|
|
71
|
+
curl -sSf https://temporal.download/cli.sh | sh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Windows:**
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
# Using Scoop
|
|
78
|
+
scoop install temporal
|
|
79
|
+
|
|
80
|
+
# Or download from releases
|
|
81
|
+
# https://github.com/temporalio/cli/releases
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Verify the installation:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
temporal --version
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Installing overmind (optional)
|
|
91
|
+
|
|
92
|
+
Overmind is a process manager that makes it easy to run multiple services together. If you want to use `overmind start` to run everything at once, install it for your platform:
|
|
93
|
+
|
|
94
|
+
**macOS:**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
brew install overmind
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Linux:**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
brew install overmind
|
|
104
|
+
|
|
105
|
+
# Or download from releases
|
|
106
|
+
# https://github.com/DarthSim/overmind/releases
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Windows:**
|
|
110
|
+
Overmind is not officially supported on Windows. Use the manual approach below instead.
|
|
111
|
+
|
|
112
|
+
## Setup
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Copy and configure environment
|
|
116
|
+
cp .env.example .env
|
|
117
|
+
# Edit .env with your BRAINTRUST_API_KEY
|
|
118
|
+
|
|
119
|
+
# Install dependencies
|
|
120
|
+
pnpm install
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Running the Example
|
|
124
|
+
|
|
125
|
+
### Option 1: Using overmind
|
|
126
|
+
|
|
127
|
+
Start the temporal server and workers together:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
overmind start
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then in another terminal, run the workflow:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pnpm run client
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Option 2: Manual
|
|
140
|
+
|
|
141
|
+
1. Start the Temporal server:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
temporal server start-dev
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
2. Start the worker:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pnpm run worker
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
3. Run the client:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pnpm run client
|
|
157
|
+
|
|
158
|
+
# Or with a signal:
|
|
159
|
+
pnpm run client -- --signal
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## What Gets Traced
|
|
163
|
+
|
|
164
|
+
- **Client span**: Wraps the workflow execution call
|
|
165
|
+
- **Workflow span**: Created via sinks when the workflow starts
|
|
166
|
+
- **Activity spans**: Created for each activity execution with parent linking
|
|
167
|
+
|
|
168
|
+
The trace hierarchy looks like:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Client span ("example.temporal.workflow")
|
|
172
|
+
└── Workflow span ("temporal.workflow.simpleWorkflow")
|
|
173
|
+
└── Activity span ("temporal.activity.addTen")
|
|
174
|
+
└── Activity span ("temporal.activity.multiplyByTwo")
|
|
175
|
+
└── Activity span ("temporal.activity.subtractFive")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## How It Works
|
|
179
|
+
|
|
180
|
+
1. **Client interceptor**: Captures the current Braintrust span context and adds it to workflow headers
|
|
181
|
+
2. **Workflow interceptor**: Extracts parent context from headers and creates a workflow span via sinks
|
|
182
|
+
3. **Sinks**: Allow the workflow isolate to call into Node.js to create spans (with `callDuringReplay: false`)
|
|
183
|
+
4. **Activity interceptor**: Creates spans for each activity, using the workflow span as parent
|