@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Mise will automatically read and use .tool-versions files as well as this file.
|
|
2
|
+
[settings]
|
|
3
|
+
experimental=true
|
|
4
|
+
|
|
5
|
+
[env]
|
|
6
|
+
# See env.example to configure API keys.
|
|
7
|
+
_.file = ".env"
|
|
8
|
+
|
|
9
|
+
[tools]
|
|
10
|
+
node = "20"
|
|
11
|
+
temporal = "latest"
|
|
12
|
+
overmind = "latest"
|
|
13
|
+
|
|
14
|
+
[hooks]
|
|
15
|
+
postinstall = "mise run install"
|
|
16
|
+
|
|
17
|
+
[tasks.install]
|
|
18
|
+
description = "Install dependencies"
|
|
19
|
+
run = "pnpm install --ignore-workspace"
|
|
20
|
+
|
|
21
|
+
[tasks.server]
|
|
22
|
+
description = "Run temporal server and workers"
|
|
23
|
+
run = "overmind s"
|
|
24
|
+
|
|
25
|
+
[tasks.workflow]
|
|
26
|
+
description = "Run workflow client"
|
|
27
|
+
run = "pnpm exec ts-node src/client.ts"
|
|
28
|
+
|
|
29
|
+
[tasks.stop]
|
|
30
|
+
description = "Stop temporal server and workers"
|
|
31
|
+
run = "overmind quit || true"
|
|
32
|
+
|
|
33
|
+
[tasks.kill]
|
|
34
|
+
description = "Force kill temporal server and workers"
|
|
35
|
+
run = """
|
|
36
|
+
pkill -f 'examples/temporal.*ts-node' 2>/dev/null || true
|
|
37
|
+
pkill -f 'overmind.*temporal' 2>/dev/null || true
|
|
38
|
+
rm -f .overmind.sock
|
|
39
|
+
echo 'Server killed'
|
|
40
|
+
"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "temporal-example",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"worker": "pnpm exec ts-node src/worker.ts",
|
|
8
|
+
"client": "pnpm exec ts-node src/client.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@braintrust/temporal": "^0.1.0",
|
|
12
|
+
"@temporalio/activity": "^1.11.0",
|
|
13
|
+
"@temporalio/client": "^1.11.0",
|
|
14
|
+
"@temporalio/common": "^1.11.0",
|
|
15
|
+
"@temporalio/worker": "^1.11.0",
|
|
16
|
+
"@temporalio/workflow": "^1.11.0",
|
|
17
|
+
"braintrust": "^2.0.0",
|
|
18
|
+
"uuid": "^9.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"@types/uuid": "^9.0.0",
|
|
23
|
+
"ts-node": "^10.9.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as braintrust from "braintrust";
|
|
2
|
+
|
|
3
|
+
export interface TaskInput {
|
|
4
|
+
value: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function addTen(input: TaskInput): Promise<number> {
|
|
8
|
+
console.log(`Adding 10 to ${input.value}`);
|
|
9
|
+
|
|
10
|
+
// Test child span within activity
|
|
11
|
+
const result = await braintrust.traced(
|
|
12
|
+
async (span) => {
|
|
13
|
+
span.log({
|
|
14
|
+
input: { value: input.value, operation: "add", operand: 10 },
|
|
15
|
+
});
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
17
|
+
const sum = input.value + 10;
|
|
18
|
+
span.log({ output: sum });
|
|
19
|
+
return sum;
|
|
20
|
+
},
|
|
21
|
+
{ name: "compute.addition" },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
console.log(`Result: ${input.value} + 10 = ${result}`);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function multiplyByTwo(input: TaskInput): Promise<number> {
|
|
29
|
+
console.log(`Multiplying ${input.value} by 2`);
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
31
|
+
const result = input.value * 2;
|
|
32
|
+
console.log(`Result: ${input.value} * 2 = ${result}`);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function subtractFive(input: TaskInput): Promise<number> {
|
|
37
|
+
console.log(`Subtracting 5 from ${input.value}`);
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
39
|
+
const result = input.value - 5;
|
|
40
|
+
console.log(`Result: ${input.value} - 5 = ${result}`);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Client, Connection } from "@temporalio/client";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
import * as braintrust from "braintrust";
|
|
4
|
+
import { BraintrustTemporalPlugin } from "@braintrust/temporal";
|
|
5
|
+
import { simpleWorkflow } from "./workflows";
|
|
6
|
+
import type { TaskInput } from "./activities";
|
|
7
|
+
|
|
8
|
+
const TASK_QUEUE = "braintrust-example-task-queue";
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
braintrust.initLogger({ projectName: "temporal-example" });
|
|
12
|
+
|
|
13
|
+
const connection = await Connection.connect({
|
|
14
|
+
address: "localhost:7233",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const client = new Client({
|
|
18
|
+
connection,
|
|
19
|
+
namespace: "default",
|
|
20
|
+
plugins: [new BraintrustTemporalPlugin()],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const inputData: TaskInput = { value: 5 };
|
|
24
|
+
const workflowId = `simple-workflow-${uuid().slice(0, 8)}`;
|
|
25
|
+
|
|
26
|
+
console.log(`Starting workflow with value: ${inputData.value}`);
|
|
27
|
+
console.log(`Workflow ID: ${workflowId}`);
|
|
28
|
+
|
|
29
|
+
// Wrap in a Braintrust span
|
|
30
|
+
await braintrust.traced(
|
|
31
|
+
async (span) => {
|
|
32
|
+
const handle = await client.workflow.start(simpleWorkflow, {
|
|
33
|
+
args: [inputData],
|
|
34
|
+
taskQueue: TASK_QUEUE,
|
|
35
|
+
workflowId,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = await handle.result();
|
|
39
|
+
span.log({ output: result });
|
|
40
|
+
console.log(`\nResult: ${result}`);
|
|
41
|
+
console.log(`\nView trace: ${span.link()}`);
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
{ name: "temporal.client.simpleWorkflow" },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await braintrust.flush();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((err) => {
|
|
51
|
+
console.error(err);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Worker, NativeConnection } from "@temporalio/worker";
|
|
2
|
+
import * as braintrust from "braintrust";
|
|
3
|
+
import { BraintrustTemporalPlugin } from "@braintrust/temporal";
|
|
4
|
+
import * as activities from "./activities";
|
|
5
|
+
|
|
6
|
+
const TASK_QUEUE = "braintrust-example-task-queue";
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
braintrust.initLogger({ projectName: "temporal-example" });
|
|
10
|
+
|
|
11
|
+
const connection = await NativeConnection.connect({
|
|
12
|
+
address: "localhost:7233",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const worker = await Worker.create({
|
|
16
|
+
connection,
|
|
17
|
+
namespace: "default",
|
|
18
|
+
taskQueue: TASK_QUEUE,
|
|
19
|
+
workflowsPath: require.resolve("./workflows"),
|
|
20
|
+
activities,
|
|
21
|
+
plugins: [new BraintrustTemporalPlugin()],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log(`Worker started on task queue: ${TASK_QUEUE}`);
|
|
25
|
+
await worker.run();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main().catch((err) => {
|
|
29
|
+
console.error(err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
proxyActivities,
|
|
3
|
+
sleep,
|
|
4
|
+
workflowInfo,
|
|
5
|
+
defineSignal,
|
|
6
|
+
setHandler,
|
|
7
|
+
log,
|
|
8
|
+
} from "@temporalio/workflow";
|
|
9
|
+
import type * as activities from "./activities";
|
|
10
|
+
|
|
11
|
+
const { addTen, multiplyByTwo, subtractFive } = proxyActivities<
|
|
12
|
+
typeof activities
|
|
13
|
+
>({
|
|
14
|
+
startToCloseTimeout: "10 seconds",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const addSignalValue = defineSignal<[number]>("addSignalValue");
|
|
18
|
+
|
|
19
|
+
export interface TaskInput {
|
|
20
|
+
value: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function simpleWorkflow(input: TaskInput): Promise<string> {
|
|
24
|
+
log.info(`Starting workflow with value: ${input.value}`);
|
|
25
|
+
|
|
26
|
+
let signalValue = 0;
|
|
27
|
+
setHandler(addSignalValue, (value: number) => {
|
|
28
|
+
log.info(`Received signal with value: ${value}`);
|
|
29
|
+
signalValue += value;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Step 1: Add 10
|
|
33
|
+
const step1 = await addTen({ value: input.value });
|
|
34
|
+
log.info(`After step 1: ${step1}`);
|
|
35
|
+
|
|
36
|
+
// Step 2: Multiply by 2
|
|
37
|
+
const step2 = await multiplyByTwo({ value: step1 });
|
|
38
|
+
log.info(`After step 2: ${step2}`);
|
|
39
|
+
|
|
40
|
+
// Step 3: Subtract 5
|
|
41
|
+
const step3 = await subtractFive({ value: step2 });
|
|
42
|
+
log.info(`After step 3: ${step3}`);
|
|
43
|
+
|
|
44
|
+
const finalResult = `Complete: ${input.value} -> +10=${step1} -> *2=${step2} -> -5=${step3} + signal(${signalValue}) = ${step3 + signalValue}`;
|
|
45
|
+
log.info(finalResult);
|
|
46
|
+
return finalResult;
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2021",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2021"],
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@braintrust/temporal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK for integrating Braintrust with Temporal",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"module": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./workflow-interceptors": {
|
|
17
|
+
"types": "./dist/workflow-interceptors.d.ts",
|
|
18
|
+
"import": "./dist/workflow-interceptors.mjs",
|
|
19
|
+
"module": "./dist/workflow-interceptors.mjs",
|
|
20
|
+
"require": "./dist/workflow-interceptors.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"watch": "tsup --watch",
|
|
26
|
+
"clean": "rm -r dist/*",
|
|
27
|
+
"test": "vitest run"
|
|
28
|
+
},
|
|
29
|
+
"author": "Braintrust Data Inc.",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@temporalio/activity": "^1.14.1",
|
|
33
|
+
"@temporalio/client": "^1.14.1",
|
|
34
|
+
"@temporalio/common": "^1.14.1",
|
|
35
|
+
"@temporalio/worker": "^1.14.1",
|
|
36
|
+
"@temporalio/workflow": "^1.14.1",
|
|
37
|
+
"@types/node": "^22.15.21",
|
|
38
|
+
"braintrust": "workspace:*",
|
|
39
|
+
"tsup": "^8.5.0",
|
|
40
|
+
"typescript": "5.5.4",
|
|
41
|
+
"vitest": "^2.1.9"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@temporalio/activity": ">=1.11.0",
|
|
45
|
+
"@temporalio/client": ">=1.11.0",
|
|
46
|
+
"@temporalio/common": ">=1.11.0",
|
|
47
|
+
"@temporalio/worker": ">=1.11.0",
|
|
48
|
+
"@temporalio/workflow": ">=1.11.0",
|
|
49
|
+
"braintrust": ">=2.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"braintrust": {
|
|
53
|
+
"optional": false
|
|
54
|
+
},
|
|
55
|
+
"@temporalio/activity": {
|
|
56
|
+
"optional": false
|
|
57
|
+
},
|
|
58
|
+
"@temporalio/client": {
|
|
59
|
+
"optional": false
|
|
60
|
+
},
|
|
61
|
+
"@temporalio/common": {
|
|
62
|
+
"optional": false
|
|
63
|
+
},
|
|
64
|
+
"@temporalio/worker": {
|
|
65
|
+
"optional": false
|
|
66
|
+
},
|
|
67
|
+
"@temporalio/workflow": {
|
|
68
|
+
"optional": false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Braintrust integration for Temporal workflows and activities.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a plugin that automatically creates Braintrust spans
|
|
5
|
+
* for Temporal workflows and activities, with proper parent-child relationships
|
|
6
|
+
* across distributed workers.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { Client, Connection } from "@temporalio/client";
|
|
11
|
+
* import { Worker } from "@temporalio/worker";
|
|
12
|
+
* import * as braintrust from "braintrust";
|
|
13
|
+
* import { createBraintrustTemporalPlugin } from "@braintrust/temporal";
|
|
14
|
+
*
|
|
15
|
+
* // Initialize Braintrust logger
|
|
16
|
+
* braintrust.initLogger({ projectName: "my-project" });
|
|
17
|
+
*
|
|
18
|
+
* // Create the plugin
|
|
19
|
+
* const braintrustPlugin = createBraintrustTemporalPlugin();
|
|
20
|
+
*
|
|
21
|
+
* // Create client with the plugin
|
|
22
|
+
* const client = new Client({
|
|
23
|
+
* connection: await Connection.connect(),
|
|
24
|
+
* plugins: [braintrustPlugin],
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Create worker with the plugin
|
|
28
|
+
* const worker = await Worker.create({
|
|
29
|
+
* taskQueue: "my-queue",
|
|
30
|
+
* workflowsPath: require.resolve("./workflows"),
|
|
31
|
+
* activities,
|
|
32
|
+
* plugins: [braintrustPlugin],
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The resulting trace will show:
|
|
37
|
+
* ```
|
|
38
|
+
* trigger-workflow (client span)
|
|
39
|
+
* └── temporal.workflow.myWorkflow
|
|
40
|
+
* ├── temporal.activity.activityOne
|
|
41
|
+
* └── temporal.activity.activityTwo
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
BraintrustTemporalPlugin,
|
|
47
|
+
createBraintrustTemporalPlugin,
|
|
48
|
+
} from "./plugin";
|
|
49
|
+
|
|
50
|
+
export type { BraintrustSinks } from "./sinks";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Context } from "@temporalio/activity";
|
|
2
|
+
import type {
|
|
3
|
+
ActivityInboundCallsInterceptor,
|
|
4
|
+
ActivityExecuteInput,
|
|
5
|
+
Next,
|
|
6
|
+
ActivityInterceptors,
|
|
7
|
+
} from "@temporalio/worker";
|
|
8
|
+
import type { WorkflowClientInterceptor } from "@temporalio/client";
|
|
9
|
+
import { defaultPayloadConverter } from "@temporalio/common";
|
|
10
|
+
import * as braintrust from "braintrust";
|
|
11
|
+
import { SpanComponentsV3 } from "braintrust/util";
|
|
12
|
+
import { getWorkflowSpanExport } from "./sinks";
|
|
13
|
+
import {
|
|
14
|
+
BRAINTRUST_SPAN_HEADER,
|
|
15
|
+
BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
|
|
16
|
+
deserializeHeaderValue,
|
|
17
|
+
} from "./utils";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a client interceptor that propagates Braintrust span context to workflows.
|
|
21
|
+
* Use this when creating a Temporal Client to enable trace context propagation.
|
|
22
|
+
*/
|
|
23
|
+
export function createBraintrustClientInterceptor(): WorkflowClientInterceptor {
|
|
24
|
+
return {
|
|
25
|
+
async start(input, next) {
|
|
26
|
+
const span = braintrust.currentSpan();
|
|
27
|
+
if (span) {
|
|
28
|
+
const exported = await span.export();
|
|
29
|
+
if (exported) {
|
|
30
|
+
const payload = defaultPayloadConverter.toPayload(exported);
|
|
31
|
+
if (payload) {
|
|
32
|
+
return next({
|
|
33
|
+
...input,
|
|
34
|
+
headers: {
|
|
35
|
+
...input.headers,
|
|
36
|
+
[BRAINTRUST_SPAN_HEADER]: payload,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return next(input);
|
|
43
|
+
},
|
|
44
|
+
async signal(input, next) {
|
|
45
|
+
return next(input);
|
|
46
|
+
},
|
|
47
|
+
async signalWithStart(input, next) {
|
|
48
|
+
const span = braintrust.currentSpan();
|
|
49
|
+
if (span) {
|
|
50
|
+
const exported = await span.export();
|
|
51
|
+
if (exported) {
|
|
52
|
+
const payload = defaultPayloadConverter.toPayload(exported);
|
|
53
|
+
if (payload) {
|
|
54
|
+
return next({
|
|
55
|
+
...input,
|
|
56
|
+
headers: {
|
|
57
|
+
...input.headers,
|
|
58
|
+
[BRAINTRUST_SPAN_HEADER]: payload,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return next(input);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Activity interceptor that creates Braintrust spans for activity executions.
|
|
71
|
+
*/
|
|
72
|
+
class BraintrustActivityInterceptor implements ActivityInboundCallsInterceptor {
|
|
73
|
+
constructor(private ctx: Context) {}
|
|
74
|
+
|
|
75
|
+
async execute(
|
|
76
|
+
input: ActivityExecuteInput,
|
|
77
|
+
next: Next<ActivityInboundCallsInterceptor, "execute">,
|
|
78
|
+
): Promise<unknown> {
|
|
79
|
+
const info = this.ctx.info;
|
|
80
|
+
const runId = info.workflowExecution.runId;
|
|
81
|
+
|
|
82
|
+
// Try to get workflow span export - first check local Map, then headers
|
|
83
|
+
let parent: string | undefined;
|
|
84
|
+
|
|
85
|
+
// Check if we have the workflow span export locally (same worker as workflow)
|
|
86
|
+
const spanExportPromise = getWorkflowSpanExport(runId);
|
|
87
|
+
if (spanExportPromise) {
|
|
88
|
+
try {
|
|
89
|
+
parent = await spanExportPromise;
|
|
90
|
+
} catch {
|
|
91
|
+
// Ignore errors, fall through to header check
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// For cross-worker activities: construct parent from workflow span ID + client context
|
|
96
|
+
if (!parent && input.headers) {
|
|
97
|
+
const workflowSpanId = deserializeHeaderValue(
|
|
98
|
+
input.headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER],
|
|
99
|
+
);
|
|
100
|
+
const clientContext = deserializeHeaderValue(
|
|
101
|
+
input.headers[BRAINTRUST_SPAN_HEADER],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (workflowSpanId && clientContext) {
|
|
105
|
+
try {
|
|
106
|
+
const clientComponents = SpanComponentsV3.fromStr(clientContext);
|
|
107
|
+
const clientData = clientComponents.data;
|
|
108
|
+
|
|
109
|
+
// We can only construct a workflow parent if we have:
|
|
110
|
+
// 1. Tracing context (root_span_id)
|
|
111
|
+
// 2. Object metadata (object_id or compute_object_metadata_args)
|
|
112
|
+
const hasTracingContext = !!clientData.root_span_id;
|
|
113
|
+
const hasObjectMetadata =
|
|
114
|
+
!!clientData.object_id || !!clientData.compute_object_metadata_args;
|
|
115
|
+
|
|
116
|
+
if (hasTracingContext && hasObjectMetadata) {
|
|
117
|
+
// Construct workflow parent with the workflow's span ID
|
|
118
|
+
// IMPORTANT: row_id must match span_id for the parent span
|
|
119
|
+
// Must provide EITHER object_id OR compute_object_metadata_args, not both
|
|
120
|
+
const workflowComponents = new SpanComponentsV3({
|
|
121
|
+
object_type: clientData.object_type,
|
|
122
|
+
object_id: clientData.object_id || undefined,
|
|
123
|
+
compute_object_metadata_args: clientData.object_id
|
|
124
|
+
? undefined
|
|
125
|
+
: clientData.compute_object_metadata_args || undefined,
|
|
126
|
+
propagated_event: clientData.propagated_event,
|
|
127
|
+
row_id: workflowSpanId, // Use workflow's row_id, not client's
|
|
128
|
+
span_id: workflowSpanId, // Use workflow's span_id, not client's
|
|
129
|
+
root_span_id: clientData.root_span_id, // Keep same trace
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
parent = workflowComponents.toStr();
|
|
133
|
+
} else {
|
|
134
|
+
// Client context doesn't have root_span_id, use it directly
|
|
135
|
+
parent = clientContext;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Fall back to client context if parsing fails
|
|
139
|
+
parent = clientContext;
|
|
140
|
+
}
|
|
141
|
+
} else if (clientContext) {
|
|
142
|
+
// No workflow span ID, use client context directly
|
|
143
|
+
parent = clientContext;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const span = braintrust.startSpan({
|
|
148
|
+
name: `temporal.activity.${info.activityType}`,
|
|
149
|
+
spanAttributes: { type: "task" },
|
|
150
|
+
parent,
|
|
151
|
+
event: {
|
|
152
|
+
metadata: {
|
|
153
|
+
"temporal.activity_type": info.activityType,
|
|
154
|
+
"temporal.activity_id": info.activityId,
|
|
155
|
+
"temporal.workflow_id": info.workflowExecution.workflowId,
|
|
156
|
+
"temporal.workflow_run_id": runId,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const result = await braintrust.withCurrent(span, () => next(input));
|
|
163
|
+
span.log({ output: result });
|
|
164
|
+
span.end();
|
|
165
|
+
return result;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
span.log({ error: String(e) });
|
|
168
|
+
span.end();
|
|
169
|
+
throw e;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create an activity interceptor factory for use with Worker.create().
|
|
176
|
+
* This factory creates BraintrustActivityInterceptor instances for each activity.
|
|
177
|
+
*/
|
|
178
|
+
export function createBraintrustActivityInterceptor(
|
|
179
|
+
ctx: Context,
|
|
180
|
+
): ActivityInterceptors {
|
|
181
|
+
return {
|
|
182
|
+
inbound: new BraintrustActivityInterceptor(ctx),
|
|
183
|
+
};
|
|
184
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ClientPlugin, ClientOptions } from "@temporalio/client";
|
|
2
|
+
import type { WorkerPlugin, WorkerOptions } from "@temporalio/worker";
|
|
3
|
+
import {
|
|
4
|
+
createBraintrustClientInterceptor,
|
|
5
|
+
createBraintrustActivityInterceptor,
|
|
6
|
+
} from "./interceptors";
|
|
7
|
+
import { createBraintrustSinks } from "./sinks";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A Braintrust plugin for Temporal that automatically instruments
|
|
11
|
+
* workflows and activities with tracing spans.
|
|
12
|
+
*
|
|
13
|
+
* This plugin implements both ClientPlugin and WorkerPlugin interfaces,
|
|
14
|
+
* so it can be used with both Temporal Client and Worker.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { Client, Connection } from "@temporalio/client";
|
|
19
|
+
* import { Worker } from "@temporalio/worker";
|
|
20
|
+
* import * as braintrust from "braintrust";
|
|
21
|
+
* import { BraintrustTemporalPlugin } from "@braintrust/temporal";
|
|
22
|
+
*
|
|
23
|
+
* // Initialize Braintrust logger
|
|
24
|
+
* braintrust.initLogger({ projectName: "my-project" });
|
|
25
|
+
*
|
|
26
|
+
* // Create client with the plugin
|
|
27
|
+
* const client = new Client({
|
|
28
|
+
* connection: await Connection.connect(),
|
|
29
|
+
* plugins: [new BraintrustTemporalPlugin()],
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Create worker with the plugin
|
|
33
|
+
* const worker = await Worker.create({
|
|
34
|
+
* taskQueue: "my-queue",
|
|
35
|
+
* workflowsPath: require.resolve("./workflows"),
|
|
36
|
+
* activities,
|
|
37
|
+
* plugins: [new BraintrustTemporalPlugin()],
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
|
|
42
|
+
get name(): string {
|
|
43
|
+
return "braintrust";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Configure the Temporal Client with Braintrust interceptors.
|
|
48
|
+
* Adds the client interceptor for propagating span context to workflows.
|
|
49
|
+
*/
|
|
50
|
+
configureClient(
|
|
51
|
+
options: Omit<ClientOptions, "plugins">,
|
|
52
|
+
): Omit<ClientOptions, "plugins"> {
|
|
53
|
+
const existing = options.interceptors?.workflow;
|
|
54
|
+
const braintrustInterceptor = createBraintrustClientInterceptor();
|
|
55
|
+
|
|
56
|
+
// workflow can be an array or an object with named interceptors
|
|
57
|
+
let workflow: typeof existing;
|
|
58
|
+
if (Array.isArray(existing)) {
|
|
59
|
+
workflow = [...existing, braintrustInterceptor];
|
|
60
|
+
} else if (existing) {
|
|
61
|
+
// It's a WorkflowClientInterceptors object, merge our interceptor
|
|
62
|
+
workflow = {
|
|
63
|
+
...existing,
|
|
64
|
+
...braintrustInterceptor,
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
workflow = [braintrustInterceptor];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...options,
|
|
72
|
+
interceptors: {
|
|
73
|
+
...options.interceptors,
|
|
74
|
+
workflow,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Configure the Temporal Worker with Braintrust interceptors and sinks.
|
|
81
|
+
* Adds the activity interceptor for creating spans, the sinks for workflow spans,
|
|
82
|
+
* and the workflow interceptor modules for bundling.
|
|
83
|
+
*/
|
|
84
|
+
configureWorker(options: WorkerOptions): WorkerOptions {
|
|
85
|
+
const existingActivityInterceptors = options.interceptors?.activity ?? [];
|
|
86
|
+
const existingWorkflowModules = options.interceptors?.workflowModules ?? [];
|
|
87
|
+
const existingSinks = options.sinks ?? {};
|
|
88
|
+
|
|
89
|
+
const braintrustSinks = createBraintrustSinks();
|
|
90
|
+
|
|
91
|
+
// Resolve the workflow interceptors module path
|
|
92
|
+
// This needs to be resolved at runtime to get the actual file path
|
|
93
|
+
const workflowInterceptorsPath = require.resolve(
|
|
94
|
+
"@braintrust/temporal/workflow-interceptors",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...options,
|
|
99
|
+
interceptors: {
|
|
100
|
+
...options.interceptors,
|
|
101
|
+
activity: [
|
|
102
|
+
...existingActivityInterceptors,
|
|
103
|
+
createBraintrustActivityInterceptor,
|
|
104
|
+
],
|
|
105
|
+
workflowModules: [...existingWorkflowModules, workflowInterceptorsPath],
|
|
106
|
+
},
|
|
107
|
+
sinks: {
|
|
108
|
+
...existingSinks,
|
|
109
|
+
...braintrustSinks,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a Braintrust plugin for Temporal.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const plugin = createBraintrustTemporalPlugin();
|
|
121
|
+
*
|
|
122
|
+
* const client = new Client({ plugins: [plugin] });
|
|
123
|
+
* const worker = await Worker.create({ plugins: [plugin], ... });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function createBraintrustTemporalPlugin(): BraintrustTemporalPlugin {
|
|
127
|
+
return new BraintrustTemporalPlugin();
|
|
128
|
+
}
|