Skip to main content
While tracing setup automatically logs 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

Add metadata and tags

Enrich spans with custom metadata and tags to make them easier to filter and analyze.
Tags from all spans in a trace are aggregated together at the trace level and automatically merged (union) rather than replaced.

Within a span

Attach metadata and tags from within the function body. This is useful for data that’s only available during execution, like computed values or results from intermediate steps.
  • In TypeScript and Python, use span.log().
  • In Go, C#, Ruby, and Java, use the OTel setAttribute API. Custom attributes appear in the span’s metadata field. Use braintrust.tags for tags. For LLM-specific OTel attributes, see OpenTelemetry.
import OpenAI from "openai";
import { initLogger, wrapTraced } from "braintrust";

const logger = initLogger({ projectName: "My Project" });
const openai = new OpenAI();

const processDocument = wrapTraced(async function processDocument(
  docId: string,
  span,
) {
  // Add metadata and tags
  span.log({
    metadata: {
      documentId: docId,
      processingType: "summarization",
      userId: "user-123",
    },
    tags: ["document-processing", "summarization"],
  });

  const response = await openai.responses.create({
    model: "gpt-5-mini",
    input: `Summarize document ${docId}`,
  });

  return response.output_text;
});

await processDocument("doc-123");

At span creation

The TypeScript and Python SDKs support passing metadata and tags at span creation time, which avoids a separate span.log() call. This is useful at request entry points where you have request-scoped data — like a user ID or org ID — already available and don’t want to thread it through helper functions.
import OpenAI from "openai";
import { initLogger } from "braintrust";

const logger = initLogger({ projectName: "My Project" });
const openai = new OpenAI();

async function handleRequest(userId: string, orgId: string, prompt: string) {
  return logger.traced(
    async (span) => {
      const response = await openai.responses.create({
        model: "gpt-5-mini",
        input: prompt,
      });
      return response.output_text;
    },
    {
      event: {
        metadata: { userId, orgId },
        tags: ["handle-request"],
      },
    },
  );
}

await handleRequest("user-123", "org-456", "What is the capital of France?");

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" },
  );
}
Span names must always be strings. Passing non-string values will cause validation failures. See Troubleshooting for details.

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.

Troubleshooting

If you pass a non-string value (like an object or array) to the name field of a span, your logs will not appear in the UI - they will be hidden due to schema validation failure. Span names must always be strings.Before passing a value to the name parameter in tracing functions, ensure it is a string:
// ❌ Wrong - passing an object
await logger.traced(
  async (span) => { /* ... */ },
  { name: { operation: "process" } } // This will fail validation
);

// ✅ Correct - passing a string
await logger.traced(
  async (span) => { /* ... */ },
  { name: "process" }
);

// ✅ Correct - validating dynamic names
const spanName = typeof customName === "string"
  ? customName
  : String(customName);

await logger.traced(
  async (span) => { /* ... */ },
  { name: spanName }
);

Next steps