Tracing
Braintrust traces Apollo Server operations through an OpenTelemetry pipeline. Install the SDK and dependencies, then attach Braintrust’s span processor and instrument your resolvers.Setup
Install the Braintrust TypeScript SDK with the OpenTelemetry dependencies, then configure your environment. This integration uses Braintrust’s TypeScript SDK OpenTelemetry configuration.1
Install packages
Install the Braintrust TypeScript SDK alongside the OpenTelemetry, Apollo Server, and utility dependencies.
pnpm add braintrust @braintrust/otel @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace-base @apollo/server @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions dotenv
npm install braintrust @braintrust/otel @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace-base @apollo/server @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions dotenv
2
Set environment variables
.env
# Required
BRAINTRUST_API_KEY=your-api-key
# Parent identifier for organizing traces
# Format: project_name:experiment_name
BRAINTRUST_PARENT=project_name:apollo-graphql
# Optional: Custom OpenTelemetry endpoint (for self-hosted Braintrust)
# BRAINTRUST_OTEL_ENDPOINT=https://api.braintrust.dev/otel/v1/traces
OpenTelemetry
Configure a tracer provider with Braintrust’s span processor, then create spans in your resolvers. Add the following toapollo-graphql-braintrust.ts:import { trace } from "@opentelemetry/api";
import { SpanStatusCode } from "@opentelemetry/api";
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { BraintrustSpanProcessor } from "@braintrust/otel";
const provider = new BasicTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: "graphql-api",
}),
spanProcessors: [new BraintrustSpanProcessor()]
});
provider.register();
// Get a tracer for your GraphQL operations
const tracer = trace.getTracer("apollo-graphql", "1.0.0");
// Import Apollo Server
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
// Define your schema
const typeDefs = `#graphql
type Query {
hello(name: String): String!
books: [Book!]!
book(id: ID!): Book
}
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Mutation {
addBook(title: String!, author: String!, year: Int): Book!
}
`;
// Mock data functions (replace with your actual implementation)
async function fetchBooks() {
return [
{
id: "1",
title: "The Great Gatsby",
author: "F. Scott Fitzgerald",
year: 1925,
},
{ id: "2", title: "1984", author: "George Orwell", year: 1949 },
];
}
async function fetchBookById(id: string) {
const books = await fetchBooks();
return books.find((book) => book.id === id);
}
async function createBook({
title,
author,
year,
}: {
title: string;
author: string;
year?: number;
}) {
return {
id: String(Date.now()),
title,
author,
year: year || new Date().getFullYear(),
};
}
// Implement resolvers with tracing
const resolvers = {
Query: {
hello: (_: any, { name }: { name?: string }) => {
// Create a span for this resolver
const span = tracer.startSpan("query.hello");
span.setAttributes({
"graphql.operation": "query",
"graphql.field": "hello",
"input.name": name || "undefined",
});
try {
const result = `Hello, ${name || "World"}!`;
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
},
books: async () => {
const span = tracer.startSpan("query.books");
span.setAttributes({
"graphql.operation": "query",
"graphql.field": "books",
});
try {
const books = await fetchBooks();
span.setAttribute("books.count", books.length);
span.setStatus({ code: SpanStatusCode.OK });
return books;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
},
book: async (_: any, { id }: { id: string }) => {
const span = tracer.startSpan("query.book");
span.setAttributes({
"graphql.operation": "query",
"graphql.field": "book",
"book.id": id,
});
try {
const book = await fetchBookById(id);
span.setAttribute("book.found", book ? "true" : "false");
span.setStatus({ code: SpanStatusCode.OK });
return book;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
},
},
Mutation: {
addBook: async (
_: any,
{ title, author, year }: { title: string; author: string; year?: number },
) => {
const span = tracer.startSpan("mutation.addBook");
span.setAttributes({
"graphql.operation": "mutation",
"graphql.field": "addBook",
"book.title": title,
"book.author": author,
});
if (year) span.setAttribute("book.year", year);
try {
const newBook = await createBook({ title, author, year });
span.setAttribute("book.id", newBook.id);
span.setStatus({ code: SpanStatusCode.OK });
return newBook;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
},
},
};
// Create and start Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });
async function main() {
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => {
// Create a root span for each GraphQL request
const rootSpan = tracer.startSpan("graphql.request");
rootSpan.setAttribute("http.method", req.method || "POST");
rootSpan.setAttribute("http.url", req.url || "/graphql");
// The span will be automatically exported when it ends
// BraintrustSpanProcessor handles the batching and sending
setTimeout(() => rootSpan.end(), 100);
return {};
},
});
console.log(`🚀 Server ready at: ${url}`);
}
main().catch(console.error);
Tracing resources
Apollo Router
Apollo Router is a language-neutral federation gateway configured through a YAML file. To trace it, enable its OpenTelemetry exporter and point OTLP at Braintrust. Add the following toapollo-router-braintrust.yaml:
apollo-router-braintrust.yaml
connectors:
preview_connect_v0_3: true
sources:
# OpenAI API configuration
openai:
$config:
apiKey: ${env.OPENAI_API_KEY}
# OpenTelemetry configuration for Braintrust
telemetry:
exporters:
tracing:
# OTLP exporter for Braintrust
otlp:
enabled: true
# Braintrust OTEL endpoint
endpoint: ${env.BRAINTRUST_OTEL_ENDPOINT:-https://api.braintrust.dev/otel}
# Use HTTP protocol for Braintrust
protocol: http
# HTTP configuration with headers for Braintrust
http:
headers:
# Braintrust API authentication
Authorization: Bearer ${env.BRAINTRUST_API_KEY}
# Parent project/experiment for traces
x-bt-parent: ${env.BRAINTRUST_PARENT:-project_name:apollo-graphql-project}
# Batch processor configuration for optimal performance
batch_processor:
scheduled_delay: 5s
max_concurrent_exports: 2
max_export_batch_size: 512
max_export_timeout: 30s
max_queue_size: 2048
metrics:
# Prometheus endpoint for local debugging
prometheus:
enabled: true
listen: 0.0.0.0:9090
path: /metrics
# Optional: OTLP metrics to Braintrust
otlp:
enabled: false
endpoint: ${env.BRAINTRUST_OTEL_ENDPOINT:-https://api.braintrust.dev/otel}
protocol: http
http:
headers:
Authorization: Bearer ${env.BRAINTRUST_API_KEY}
x-bt-parent: ${env.BRAINTRUST_PARENT:-project_name:apollo-graphql-project}
# Include subgraph errors in responses for debugging
include_subgraph_errors:
all: true
# GraphQL query limits
limits:
max_depth: 20
max_height: 200
max_aliases: 30
max_root_fields: 30