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

# Application logs

> Emit application logs from the Python SDK or forward standard-library logging records, then associate them with the traces they belong to.

export const feature_0 = "Application logs"

export const verb_0 = "are"

<Warning>
  {feature_0} {verb_0} in [public preview](/docs/feature-lifecycle) and can change before reaching general availability.
</Warning>

<Note>
  Requires Python SDK v0.42.0 or later, and `BRAINTRUST_API_KEY` set to a [Braintrust API key](/docs/admin/authentication#api-authentication).
</Note>

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

## 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.

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from braintrust import init_logger

logger = init_logger(project="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"})

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` attributes | `metadata` |

The body can be any JSON-serializable value, not just a string.

### Preserve message templates

Pass named parameters to group messages by a stable template while retaining each event's values:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from braintrust import init_logger

logger = init_logger(project="application-logs")

logger.info(
    "User {user_id} paid {amount:.2f}",
    user_id="user_123",
    amount=12.5,
    metadata={"source": "checkout"},
)

logger.flush()
```

The record stores the rendered message, template, and 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 {user_id} paid {amount:.2f}",
    "braintrust.template.parameter.user_id": "user_123",
    "braintrust.template.parameter.amount": 12.5
  }
}
```

Missing parameters remain as literal placeholders. Malformed braces or unsupported format specifiers leave the original message body unchanged. Formatting problems don't raise exceptions.

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
```

### Use t-string templates

On Python 3.14 and later, severity methods accept t-strings with embedded interpolation values. Passing separate named parameters with a t-string raises `TypeError`.

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
user_id = "user_123"
amount = 12.5

logger.info(
    t"User {user_id} paid {amount:.2f}",
    metadata={"source": "checkout"},
)
```

This produces the same rendered message, template, and parameters as the named-parameter form.

## Capture existing Python logs

Attach `BraintrustLogHandler` to a standard-library logger to forward its records without changing existing log calls. Set the logger's level to include the messages you want to capture:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import logging

from braintrust import BraintrustLogHandler, init_logger

braintrust_logger = init_logger(project="application-logs")
handler = BraintrustLogHandler(braintrust_logger)
app_logger = logging.getLogger("checkout")
app_logger.setLevel(logging.INFO)
app_logger.addHandler(handler)

app_logger.info("Payment %s started", "pay_123", extra={"attempt": 1})

handler.flush()
app_logger.removeHandler(handler)
handler.close()
```

The handler preserves the formatted message (including exception information), original timestamp, template parameters, and `extra` fields. It also records the logger name and source location.

To prevent recursive logging, the handler ignores records from the `braintrust` logger and records emitted while the SDK's HTTP transport is active.

## 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:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from braintrust import init_logger

logger = init_logger(project="application-logs")

with logger.start_span(name="process-payment"):
    logger.warn("Retrying payment", metadata={"attempt": 2})

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.
