> ## Documentation Index
> Fetch the complete documentation index at: https://www.braintrust.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Application logs

> Emit application logs from the TypeScript SDK or forward console output, then associate them with the traces they belong to.

> [!WARNING]
>
> Application logs are in [public preview](/docs/feature-lifecycle) and can change before reaching general availability.

> [!NOTE]
>
> Requires TypeScript SDK v3.39.0 or later, and `BRAINTRUST_API_KEY` set to a [Braintrust API key](/docs/admin/authentication#api-authentication).

Use the TypeScript SDK to send [application logs](/docs/instrument/application-logs) to Braintrust. Emit messages directly from a project logger, or forward the messages your code already writes with `console`.

## Emit logs from the SDK

To add log statements to your code, initialize a project logger and call a severity method: `trace()`, `debug()`, `info()`, `warn()`, `error()`, or `fatal()`. The `trace()` method emits a message at the `trace` severity level. It doesn't start a Braintrust trace.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { initLogger } from "braintrust";

const logger = initLogger({ projectName: "application-logs" });

logger.info("Worker started", { metadata: { worker_id: "worker-1" } });
logger.warn("Retrying request", { metadata: { attempt: 2 } });
logger.error("Payment failed", { metadata: { payment_id: "pay_123" } });

await logger.flush();
```

Each call creates one row with `span_attributes.type` set to `log`:

| What you pass | Where it's stored |
| - | - |
| Message body | `output` |
| Severity method you call | `span_attributes.log_level` |
| `metadata` option | `metadata` |

The body can be any JSON-serializable value, not just a string. Each method returns the row ID. With `asyncFlush: false`, it returns a promise that resolves to the row ID after the row is flushed.

### Preserve message templates

Pass a template literal tagged with `logger.fmt` to group messages by a stable template while retaining each event's values. Format values with JavaScript expressions inside the template:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { initLogger } from "braintrust";

const logger = initLogger({ projectName: "application-logs" });

const userId = "user_123";
const amount = 12.5;

logger.info(logger.fmt`User ${userId} paid ${amount.toFixed(2)}`, {
  metadata: { source: "checkout" },
});

await logger.flush();
```

The record stores the rendered message, the template with numbered placeholders, and the parameters:

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "output": "User user_123 paid 12.50",
  "span_attributes": {
    "type": "log",
    "log_level": "info"
  },
  "metadata": {
    "source": "checkout",
    "braintrust.template": "User ${0} paid ${1}",
    "braintrust.template.parameter.0": "user_123",
    "braintrust.template.parameter.1": "12.50"
  }
}
```

Parameters keep their JSON value types, and `undefined` and `BigInt` values are stored as strings. Template metadata replaces keys of the same name in the `metadata` option. A template literal without `logger.fmt` is stored as a plain message without template metadata.

Because the template stays the same across events, you can group by it to find your most frequent messages with [SQL](/docs/reference/sql):

```sql Most frequent message templates theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
SELECT metadata."braintrust.template" AS template, COUNT(*) AS occurrences
FROM project_logs('<PROJECT_ID>') -- Replace with your project ID
WHERE span_attributes.type = 'log'
  AND metadata."braintrust.template" IS NOT NULL
  AND created > NOW() - INTERVAL 7 DAY
GROUP BY template
ORDER BY occurrences DESC
```

## Capture console output

`wrapConsole()` forwards calls to `console.log()`, `console.trace()`, `console.debug()`, `console.info()`, `console.warn()`, and `console.error()` to Braintrust without changing existing log calls. Console output still appears as before.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { initLogger, wrapConsole } from "braintrust";

const logger = initLogger({ projectName: "application-logs" });
const unwrapConsole = wrapConsole();

console.info("Payment %s started", "pay_123");

unwrapConsole();
await logger.flush();
```

Each console method maps to the severity level of the same name, except `console.log()`, which maps to `info`. The record stores the formatted message in `output` and sets `logger.name` to `console` in `metadata`. When the first argument is a string followed by more arguments, the record also stores it in `braintrust.template` and each remaining argument in `braintrust.template.parameter.<index>`. String, number, boolean, and `null` arguments keep their values, and other arguments are stored as formatted text. Outside Node.js, such as in browsers, Cloudflare Workers, and edge runtimes, the message joins the arguments with spaces instead of applying format specifiers such as `%s`.

`wrapConsole()` accepts two optional arguments:

* **`target`**: the console object to wrap. Defaults to the global `console`.
* **`options.logger`**: the logger that receives console messages. Defaults to the current logger at the time of each call. If no current logger exists, for example before you initialize one or when your logger uses `setCurrent: false`, console calls produce their normal output only.

It returns a cleanup function that stops forwarding for that wrap. Calling `wrapConsole()` again on the same console updates the destination without duplicating messages. Cleanup doesn't disable a later wrap, and it doesn't flush messages that were already captured.

The SDK's own diagnostic output isn't forwarded, and a console call made from inside another console call, such as from a patched console method, is written to the console only.

## Associate logs with a trace

Emit a message inside an active Braintrust span to associate it with that operation. For example, log a retry inside the payment operation that triggered it:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { initLogger } from "braintrust";

const logger = initLogger({ projectName: "application-logs" });

await logger.traced(
  async () => {
    logger.warn("Retrying payment", { metadata: { attempt: 2 } });
  },
  { name: "process-payment" },
);

await logger.flush();
```

The message has its own row ID and shares the active span's span and trace IDs. Logs can also correlate with active OpenTelemetry spans when you configure [OpenTelemetry compatibility mode](/docs/integrations/sdk-integrations/opentelemetry/link-spans#share-context-within-a-process).

Without an active span, each SDK logger instance groups its messages under a shared trace ID. Those messages can belong to different requests. Use an active span to associate messages with a specific request or operation.

## Next steps

* [Inspect logs](/docs/instrument/application-logs#inspect-logs) in the UI or with SQL.
* [Share context with OpenTelemetry](/docs/integrations/sdk-integrations/opentelemetry/link-spans#share-context-within-a-process) to correlate logs with active OpenTelemetry spans.
* [Trace application logic](/docs/instrument/trace-application-logic) to time operations and nest logs under them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.