While AI provider wrappers automatically log LLM calls, you often need to trace additional application logic like data retrieval, preprocessing, business logic, or tool invocations. Custom tracing lets you capture these operations.
Trace function calls
Braintrust SDKs provide tools to trace function execution and capture inputs, outputs, and errors:
- Python SDK uses the
@traced decorator to automatically wrap functions
- TypeScript SDK uses
wrapTraced() to create traced function wrappers
- Go SDK uses OpenTelemetry’s manual span management with
tracer.Start() and span.End()
All approaches achieve the same result—capturing function-level observability—but with different ergonomics suited to each language’s idioms.
import { initLogger, wrapTraced } from "braintrust";
const logger = initLogger({ projectName: "My Project" });
// Wrap a function to trace it automatically
const fetchUserData = wrapTraced(async function fetchUserData(userId: string) {
// This function's input (userId) and output (return value) are logged
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
// Use the function normally
const userData = await fetchUserData("user-123");
The traced function automatically creates a span with:
- Function name as the span name
- Function arguments as input
- Return value as output
- Any errors that occur
Enrich spans with custom metadata and tags to make them easier to filter and analyze. Tags can be applied to any span in a trace, including nested spans, and are automatically aggregated at the trace level:
import { initLogger, wrapTraced } from "braintrust";
const logger = initLogger({ projectName: "My Project" });
const processDocument = wrapTraced(async function processDocument(
docId: string,
span,
) {
// Add custom metadata and tags
span.log({
metadata: {
documentId: docId,
processingType: "summarization",
userId: "user-123",
},
tags: ["document-processing", "summarization"],
});
const doc = await loadDocument(docId);
const summary = await summarize(doc);
return summary;
});
Tags from all spans in a trace are aggregated together at the trace level. When you log additional tags to the same span, they are automatically merged (union) rather than replaced, allowing you to add contextual tags throughout your application logic.
Manual spans
For more control, create spans manually using logger.traced() or startSpan():
import { initLogger } from "braintrust";
const logger = initLogger({ projectName: "My Project" });
async function complexWorkflow(input: string) {
// Create a manual span
await logger.traced(
async (span) => {
span.log({ input });
// Step 1
const data = await fetchData(input);
span.log({ metadata: { step: "fetch", recordCount: data.length } });
// Step 2
const processed = await processData(data);
span.log({ metadata: { step: "process" } });
// Log final output
span.log({ output: processed });
},
{ name: "complexWorkflow", type: "task" },
);
}
Nest spans
Spans automatically nest when called within other spans, creating a hierarchy that represents your application’s execution flow:
import { initLogger, wrapTraced } from "braintrust";
const logger = initLogger({ projectName: "My Project" });
const fetchData = wrapTraced(async function fetchData(query: string) {
// Database query logic
return await db.query(query);
});
const transformData = wrapTraced(async function transformData(data: any[]) {
// Data transformation logic
return data.map((item) => transform(item));
});
// Parent span containing child spans
const pipeline = wrapTraced(async function pipeline(input: string) {
const data = await fetchData(input); // Child span 1
const transformed = await transformData(data); // Child span 2
return transformed;
});
// Creates a trace with nested spans:
// pipeline
// └─ fetchData
// └─ transformData
await pipeline("user query");
This nesting makes it easy to see which operations happened as part of a larger workflow.
Next steps