Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/decouple-otel-sdk-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@braintrust/otel": major
"braintrust": patch
---

refactor(otel)!: Decouple from OpenTelemetry SDK versions
2 changes: 1 addition & 1 deletion .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ jobs:
fail-fast: false
matrix:
node-version: [22.18.0, 24, 26]
test-dir: [otel-v1, otel-v2]
test-dir: [otel-v1, otel-v2, otel-v3]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Docker Buildx
Expand Down
31 changes: 31 additions & 0 deletions e2e/config/pr-comment-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -1072,5 +1072,36 @@
"label": "v4 latest"
}
]
},
{
"scenarioDirName": "otel-sdk-compat",
"label": "OpenTelemetry SDK compatibility",
"metadataScenario": "otel-sdk-compat",
"variants": [
{
"variantKey": "otel-v1",
"label": "v1 pinned"
},
{
"variantKey": "otel-v1-latest",
"label": "v1 latest"
},
{
"variantKey": "otel-v2",
"label": "v2 pinned"
},
{
"variantKey": "otel-v2-latest",
"label": "v2 latest"
},
{
"variantKey": "otel-v3",
"label": "v3 pinned"
},
{
"variantKey": "otel-v3-latest",
"label": "v3 latest"
}
]
}
]
3 changes: 2 additions & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"typescript": "5.4.4",
"vitest": "catalog:",
"zod": "4.3.6",
"vite-plus": "catalog:"
"vite-plus": "catalog:",
"@opentelemetry/exporter-trace-otlp-http": "0.217.0"
}
}
13 changes: 9 additions & 4 deletions e2e/scenarios/ai-sdk-otel-export/scenario.impl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { BraintrustExporter } from "@braintrust/otel";
import { BraintrustExporter, braintrustOtlpConfig } from "@braintrust/otel";
import {
createTracerProvider,
getTestRunId,
Expand Down Expand Up @@ -32,9 +33,13 @@ export async function runAISDKOtelExport(options: AISDKOtelScenarioOptions) {
const projectName = scopedName("e2e-ai-sdk-otel-export", testRunId);

const exporter = new BraintrustExporter({
apiKey: process.env.BRAINTRUST_API_KEY!,
apiUrl: process.env.BRAINTRUST_API_URL!,
parent: `project_name:${projectName}`,
exporter: new OTLPTraceExporter(
braintrustOtlpConfig({
apiKey: process.env.BRAINTRUST_API_KEY!,
apiUrl: process.env.BRAINTRUST_API_URL!,
parent: `project_name:${projectName}`,
}),
),
filterAISpans: true,
});

Expand Down
15 changes: 10 additions & 5 deletions e2e/scenarios/nextjs-instrumentation/src/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BraintrustExporter } from "@braintrust/otel";
import { OTLPHttpJsonTraceExporter } from "@vercel/otel";
import { BraintrustExporter, braintrustOtlpConfig } from "@braintrust/otel";
import { registerOTel } from "@vercel/otel";
import { initLogger } from "braintrust";
import { scopedName } from "./lib/runtime-check";
Expand All @@ -12,10 +13,14 @@ export async function register() {
registerOTel({
serviceName: "nextjs-instrumentation-e2e",
traceExporter: new BraintrustExporter({
apiKey: process.env.BRAINTRUST_API_KEY!,
apiUrl: process.env.BRAINTRUST_API_URL!,
parent: `project_name:${scopedName("e2e-nextjs-instrumentation")}`,
exporter: new OTLPHttpJsonTraceExporter(
braintrustOtlpConfig({
apiKey: process.env.BRAINTRUST_API_KEY!,
apiUrl: process.env.BRAINTRUST_API_URL!,
parent: `project_name:${scopedName("e2e-nextjs-instrumentation")}`,
}),
),
filterAISpans: false,
}) as any,
}),
});
}
61 changes: 13 additions & 48 deletions e2e/scenarios/nextjs-instrumentation/src/lib/runtime-check.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BraintrustExporter } from "@braintrust/otel";
import { trace } from "@opentelemetry/api";
import { OTLPHttpJsonTraceExporter } from "@vercel/otel";
import { BraintrustExporter, braintrustOtlpConfig } from "@braintrust/otel";
import {
BasicTracerProvider,
SimpleSpanProcessor,
Expand Down Expand Up @@ -33,54 +33,20 @@ export function scopedName(base: string): string {
return `${base}-${suffix}`;
}

function createTracerProvider(processors: unknown[]) {
if (
typeof (new BasicTracerProvider() as { addSpanProcessor?: unknown })
.addSpanProcessor === "function"
) {
const provider = new BasicTracerProvider() as {
addSpanProcessor: (processor: unknown) => void;
getTracer: (name: string) => {
startActiveSpan: (
name: string,
callback: (span: {
end: () => void;
setAttribute: (key: string, value: string) => void;
}) => Promise<void> | void,
) => Promise<void>;
};
shutdown?: () => Promise<void>;
};
processors.forEach((processor) => provider.addSpanProcessor(processor));
return provider;
}

return new BasicTracerProvider({
spanProcessors: processors as never,
}) as {
getTracer: (name: string) => {
startActiveSpan: (
name: string,
callback: (span: {
end: () => void;
setAttribute: (key: string, value: string) => void;
}) => Promise<void> | void,
) => Promise<void>;
};
shutdown?: () => Promise<void>;
};
}

async function emitOtelSpan(runtime: Runtime, testRunId: string) {
const exporter = new BraintrustExporter({
apiKey: requiredEnv("BRAINTRUST_API_KEY"),
apiUrl: requiredEnv("BRAINTRUST_API_URL"),
parent: `project_name:${scopedName("e2e-nextjs-instrumentation")}`,
exporter: new OTLPHttpJsonTraceExporter(
braintrustOtlpConfig({
apiKey: requiredEnv("BRAINTRUST_API_KEY"),
apiUrl: requiredEnv("BRAINTRUST_API_URL"),
parent: `project_name:${scopedName("e2e-nextjs-instrumentation")}`,
}),
),
filterAISpans: false,
});

const provider = createTracerProvider([new SimpleSpanProcessor(exporter)]);
trace.setGlobalTracerProvider(provider as never);
const processor = new SimpleSpanProcessor(exporter);
const provider = new BasicTracerProvider({ spanProcessors: [processor] });

await provider
.getTracer("nextjs-instrumentation-e2e")
Expand All @@ -91,9 +57,8 @@ async function emitOtelSpan(runtime: Runtime, testRunId: string) {
span.end();
});

await exporter.forceFlush();
await provider.shutdown?.();
await new Promise((resolve) => setTimeout(resolve, 250));
await processor.forceFlush();
await provider.shutdown();
}

export async function runRuntimeCheck(runtime: Runtime) {
Expand Down
23 changes: 19 additions & 4 deletions e2e/scenarios/otel-compat-mixed-tracing/scenario.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { context as otelContext, trace } from "@opentelemetry/api";
import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks";
import { BraintrustSpanProcessor, setupOtelCompat } from "@braintrust/otel";
import {
BraintrustSpanProcessor,
setupOtelCompat,
BraintrustExporter,
braintrustOtlpConfig,
} from "@braintrust/otel";
import { initLogger, withCurrent } from "braintrust";
import {
createTracerProvider,
Expand All @@ -19,9 +26,17 @@ async function main() {

try {
const processor = new BraintrustSpanProcessor({
apiKey: process.env.BRAINTRUST_API_KEY!,
apiUrl: process.env.BRAINTRUST_API_URL!,
parent: `project_name:${scopedName("e2e-otel-compat-mixed-tracing", testRunId)}`,
processor: new BatchSpanProcessor(
new BraintrustExporter({
exporter: new OTLPTraceExporter(
braintrustOtlpConfig({
apiKey: process.env.BRAINTRUST_API_KEY!,
apiUrl: process.env.BRAINTRUST_API_URL!,
parent: `project_name:${scopedName("e2e-otel-compat-mixed-tracing", testRunId)}`,
}),
),
}),
),
});
const provider = createTracerProvider([processor]);
trace.setGlobalTracerProvider(provider);
Expand Down
82 changes: 82 additions & 0 deletions e2e/scenarios/otel-sdk-compat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"name": "@braintrust/e2e-otel-sdk-compat",
"private": true,
"braintrustScenario": {
"bump": {
"dependencies": {
"otel-v1-latest-api": {
"package": "@opentelemetry/api",
"range": "~1.9.0",
"targetDependency": "@opentelemetry/api",
"targetManifest": "versions/otel-v1-latest/package.json"
},
"otel-v1-latest-sdk-trace-base": {
"package": "@opentelemetry/sdk-trace-base",
"range": "^1.30.1",
"targetDependency": "@opentelemetry/sdk-trace-base",
"targetManifest": "versions/otel-v1-latest/package.json"
},
"otel-v1-latest-context-async-hooks": {
"package": "@opentelemetry/context-async-hooks",
"range": "^1.30.1",
"targetDependency": "@opentelemetry/context-async-hooks",
"targetManifest": "versions/otel-v1-latest/package.json"
},
"otel-v1-latest-exporter-trace-otlp-http": {
"package": "@opentelemetry/exporter-trace-otlp-http",
"range": "^0.57.2",
"targetDependency": "@opentelemetry/exporter-trace-otlp-http",
"targetManifest": "versions/otel-v1-latest/package.json"
},
"otel-v2-latest-api": {
"package": "@opentelemetry/api",
"range": "~1.9.0",
"targetDependency": "@opentelemetry/api",
"targetManifest": "versions/otel-v2-latest/package.json"
},
"otel-v2-latest-sdk-trace-base": {
"package": "@opentelemetry/sdk-trace-base",
"range": "^2.12.0",
"targetDependency": "@opentelemetry/sdk-trace-base",
"targetManifest": "versions/otel-v2-latest/package.json"
},
"otel-v2-latest-context-async-hooks": {
"package": "@opentelemetry/context-async-hooks",
"range": "^2.12.0",
"targetDependency": "@opentelemetry/context-async-hooks",
"targetManifest": "versions/otel-v2-latest/package.json"
},
"otel-v2-latest-exporter-trace-otlp-http": {
"package": "@opentelemetry/exporter-trace-otlp-http",
"range": "^0.218.0",
"targetDependency": "@opentelemetry/exporter-trace-otlp-http",
"targetManifest": "versions/otel-v2-latest/package.json"
},
"otel-v3-latest-api": {
"package": "@opentelemetry/api",
"range": "~1.10.0",
"targetDependency": "@opentelemetry/api",
"targetManifest": "versions/otel-v3-latest/package.json"
},
"otel-v3-latest-sdk-trace": {
"package": "@opentelemetry/sdk-trace",
"range": "^3.1.0",
"targetDependency": "@opentelemetry/sdk-trace",
"targetManifest": "versions/otel-v3-latest/package.json"
},
"otel-v3-latest-context-async-hooks": {
"package": "@opentelemetry/context-async-hooks",
"range": "^3.1.0",
"targetDependency": "@opentelemetry/context-async-hooks",
"targetManifest": "versions/otel-v3-latest/package.json"
},
"otel-v3-latest-exporter-trace-otlp-http": {
"package": "@opentelemetry/exporter-trace-otlp-http",
"range": "^0.301.0",
"targetDependency": "@opentelemetry/exporter-trace-otlp-http",
"targetManifest": "versions/otel-v3-latest/package.json"
}
}
}
}
}
82 changes: 82 additions & 0 deletions e2e/scenarios/otel-sdk-compat/scenario.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { expect, test } from "vite-plus/test";
import {
prepareScenarioDir,
resolveScenarioDir,
withScenarioHarness,
} from "../../helpers/scenario-harness";
import { extractOtelSpans } from "../../helpers/trace-summary";

const originalScenarioDir = resolveScenarioDir(import.meta.url);
const variants = [
"otel-v1",
"otel-v1-latest",
"otel-v2",
"otel-v2-latest",
"otel-v3",
"otel-v3-latest",
];
const prepared = await Promise.all(
variants.map(async (variant) => {
// Separate install roots prevent different OTel API peer ranges from sharing a copy.
const sourceDir = path.resolve(
originalScenarioDir,
`../../.bt-tmp/generated-scenarios/otel-sdk-compat-${variant}`,
);
await fs.rm(sourceDir, { recursive: true, force: true });
await fs.cp(path.join(originalScenarioDir, "template"), sourceDir, {
recursive: true,
});
await fs.cp(
path.join(originalScenarioDir, "versions", variant),
sourceDir,
{ recursive: true },
);
return {
variant,
scenarioDir: await prepareScenarioDir({ scenarioDir: sourceDir }),
};
}),
);

for (const { variant, scenarioDir } of prepared) {
test(`otel-sdk-compat ${variant}: native context, filtering, customization and OTLP export`, async () => {
await withScenarioHarness(
async ({ runNodeScenarioDir, requestsAfter, testRunId }) => {
await runNodeScenarioDir({
scenarioDir,
entry: "scenario.mjs",
env: { OTEL_MAJOR: variant.slice(6, 7) },
runContext: {
variantKey: variant,
originalScenarioDir,
cassette: false,
},
});
const requests = requestsAfter(
0,
(request) => request.path === "/otel/v1/traces",
);
expect(requests).toHaveLength(1);
const spans = extractOtelSpans(requests[0].jsonBody);
expect(spans.map((span) => span.name).sort()).toEqual([
"gen_ai.child",
"gen_ai.remote",
]);
for (const span of spans) {
expect(span.attributes.testRunId).toBe(testRunId);
expect(span.attributes.reviewed).toBe(true);
expect(span.attributes.secret).toBeUndefined();
expect(span.attributes["braintrust.parent"]).toBeDefined();
expect(span.parentSpanId).toBeDefined();
}
expect(spans[0].traceId).toBe(spans[1].traceId);
expect(spans[0].parentSpanId).toBe(spans[1].parentSpanId);
expect(JSON.stringify(requests[0].jsonBody)).not.toContain(
"private-payload",
);
},
);
});
}
Loading
Loading