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

# Logging

> Structured logging with zero setup: where logs go, the default format, and how to customize them.

The SDK logs every failure once, with a machine-readable error code and full context, to a local JSON file — no setup required. This page covers where logs go, what a line looks like, and how to customize them. To package logs into something you can send to Bioptimus support, see [Support bundles](/guides/get-started/support-bundles).

## What the logging layer gives you

Logging is a small, dependency-light layer with a handful of distinct jobs. Each has its own section below — start with whichever capability you need.

| You want to…                                                               | Use                                            | Section                                                             |
| -------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------- |
| Keep a durable JSON record of every failure, with zero setup               | *automatic on first log*                       | [Zero setup](#zero-setup)                                           |
| Send logs to a file, the console, a custom stream, or your app's handlers  | `configure_logging`, `BIOPTIMUS_LOG_FILE`      | [Log destinations](#log-destinations)                               |
| Know exactly what a log line contains                                      | `JsonFormatter` output                         | [Default log format](#default-log-format)                           |
| Emit your own structured events, or record every failure of your functions | `log_event`, `log_failures`                    | [Emit your own log entries](#emit-your-own-log-entries)             |
| Tag every line of a run / slide / request with shared correlation IDs      | `bind_context`, `new_trace_id`                 | [Group related log lines](#group-related-log-lines)                 |
| Read logs back as plain dicts, without parsing JSON                        | `read_recent_logs`, `read_usage_records`       | [Read your logs back](#read-your-logs-back-in-python)               |
| Change level, rotation interval, retention, or destination                 | `configure_logging`                            | [Customize logging](#customize-logging)                             |
| Emit SDK logs in your own format, to your own sink                         | your own `logging` handler (records propagate) | [Can I change the log format?](#can-i-change-the-log-format)        |
| Redact PII in the log file itself, not just on export                      | `configure_logging(anonymize=True)`            | [Redact sensitive fields at rest](#redact-sensitive-fields-at-rest) |
| Catch typed, coded errors in your own code                                 | `BioptimusError`, `ErrorCode`                  | [Handle errors in your code](#handle-errors-in-your-code)           |

Every helper on this page imports from the package root:

```python theme={null}
from bioptimus.observability import (
    configure_logging, default_log_file, get_log_file,
    log_event, log_failures,
    bind_context, get_context, new_trace_id,
    read_recent_logs, read_usage_records,
    BioptimusError, ErrorCode,
)
```

## Zero setup

<Info>
  Logs rotate daily and are kept for **14 days** by default. Change this with `configure_logging(backup_count=...)`.
</Info>

The first emitted event auto-installs a daily-rotating JSON log file under the system temp directory — nothing to configure.

```python theme={null}
from bioptimus.observability import default_log_file, get_log_file

print(default_log_file())  # the zero-setup fallback, e.g. /tmp/bioptimus.log
print(get_log_file())      # the path actually in use now — honors BIOPTIMUS_LOG_FILE and
                           # configure_logging(); None when file logging is disabled
```

<Tip>
  On first use, if logging falls back to the temp directory, the SDK emits a single `log_file_in_temp_dir` warning — that location can be wiped on reboot or by a temp-file cleaner. Point logging at a durable path (see [Customize logging](#customize-logging)) to silence it and keep a lasting record.
</Tip>

<Note>
  The log file is **not anonymized at rest** — it keeps full detail so you can debug locally, and it never contains slide images, tile pixels, or omics values, only pipeline metadata (paths, IDs, error codes, tracebacks). Anonymization of that metadata is applied only when you **export** a support bundle or query `/diagnostics` (see [Support bundles](/guides/get-started/support-bundles)).
</Note>

## Log destinations

| Destination      | How                                                                                         | Notes                                                           |
| ---------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| File (default)   | Nothing — zero setup                                                                        | Writes to `default_log_file()`, under the system temp directory |
| A different file | `BIOPTIMUS_LOG_FILE=/path/to/file.log` or `configure_logging(log_file="/path/to/file.log")` | The env var also applies to the `bioptimus` CLI                 |
| Console (stderr) | `configure_logging(console=True)`                                                           | Human-readable, one line per event                              |
| A custom stream  | `configure_logging(stream=my_stream)`                                                       | JSON lines; implies `console=True`                              |
| Disabled         | `BIOPTIMUS_LOG_FILE=` (empty)                                                               | No file sink; records still propagate to your own handlers      |

```bash theme={null}
export BIOPTIMUS_LOG_FILE=/var/log/bioptimus.log   # custom path
export BIOPTIMUS_LOG_FILE=                          # disable file logging
```

## Default log format

Every line is a single JSON object:

```json theme={null}
{
  "timestamp": "2026-09-08T12:00:00.123456+00:00",
  "level": "ERROR",
  "event": "wsi_open_failed",
  "logger": "bioptimus.io.wsi",
  "error_code": "BIO-WSI-002",
  "remediation": "Verify the slide path points to a valid, non-truncated, readable WSI file.",
  "trace_id": "01J9Z8Q3F9K2M4N6P8Q0R2S4T6",
  "run_id": "01J9Z8Q3F9K2M4N6P8Q0R2S4T7",
  "exception": "Traceback (most recent call last): ..."
}
```

| Field                                        | Always present                | Meaning                                               |
| -------------------------------------------- | ----------------------------- | ----------------------------------------------------- |
| `timestamp`                                  | Yes                           | ISO 8601, UTC                                         |
| `level`                                      | Yes                           | `INFO`, `WARNING`, `ERROR`, ...                       |
| `event`                                      | Yes                           | Stable event slug, e.g. `wsi_open_failed`             |
| `logger`                                     | Yes                           | Python logger name that emitted the record            |
| `error_code`                                 | On a `BioptimusError`         | One of the codes in [Error codes](#error-codes) below |
| `remediation`                                | When the code has one         | The fix for that code                                 |
| `trace_id` / `run_id` / `slide_id` / `stage` | When bound via `bind_context` | Correlation fields propagated down the pipeline       |
| `exception` / `stack`                        | On a failure                  | Full traceback                                        |

## Emit your own log entries

Wrap your own pipeline functions to get the same one-record-per-failure behavior with `log_failures`, or emit a one-off structured event with `log_event` — both write to the log file that's already active, no [`configure_logging`](#customize-logging) call required (see [Zero setup](#zero-setup)). In every example below, `run_slide` is *your* function and `inference` is a client you built earlier (for example `inference = Inference(...)` or `model = Backbone(...)`) — swap in your own call:

<CodeGroup>
  ```python Decorator theme={null}
  from bioptimus.observability import log_failures


  # Records any exception once (with its error code and traceback), then re-raises it.
  @log_failures
  def run_slide(path: str):
      return inference.predict(path)
  ```

  ```python Async theme={null}
  from bioptimus.observability import log_failures


  # log_failures auto-detects async functions — no separate decorator needed.
  @log_failures
  async def run_slide(path: str):
      return await inference.predict_async(path)
  ```

  ```python Custom logger/level theme={null}
  import logging

  from bioptimus.observability import log_failures

  logger = logging.getLogger(__name__)


  # Log on your own logger, and as a WARNING instead of the default ERROR.
  @log_failures(logger=logger, level=logging.WARNING)
  def run_slide(path: str):
      return inference.predict(path)
  ```

  ```python Manual event theme={null}
  import logging

  from bioptimus.observability import log_event

  logger = logging.getLogger(__name__)

  # Emit a single structured line yourself. Extra keyword args become JSON fields.
  log_event(
      logger,
      logging.WARNING,
      "slide_skipped",          # the event slug (becomes the "event" field)
      reason="tissue_mask_empty",  # any extra fields you want on the line
      slide_id="s1234",
  )
  ```
</CodeGroup>

Each path writes one JSON line to the log file. A failure caught by `log_failures` looks like:

```json theme={null}
{
  "timestamp": "2026-09-08T12:00:00.123456+00:00",
  "level": "ERROR",
  "event": "wsi_open_failed",
  "logger": "__main__",
  "error_code": "BIO-WSI-002",
  "remediation": "Verify the slide path points to a valid, non-truncated, readable WSI file.",
  "operation": "run_slide",
  "exception": "Traceback (most recent call last): ..."
}
```

and the manual `log_event` call above writes:

```json theme={null}
{
  "timestamp": "2026-09-08T12:00:00.123456+00:00",
  "level": "WARNING",
  "event": "slide_skipped",
  "logger": "__main__",
  "reason": "tissue_mask_empty",
  "slide_id": "s1234"
}
```

`log_event` also accepts `error_code=` — which auto-attaches the matching `remediation` — and `exc_info=exc` to record a caught exception's traceback on the line.

## Group related log lines

Bind correlation fields once and every log line emitted inside the block is tagged with them automatically — so all lines for a run, a slide, or a request share a `trace_id`/`slide_id` you can filter on. Fields whose value is `None` are ignored, and the previous context is restored on exit (even on error):

```python theme={null}
from bioptimus.observability import bind_context, get_context, new_trace_id

with bind_context(trace_id=new_trace_id(), slide_id="TCGA-AA-1234"):
    run_slide("slide.svs")   # every log line inside carries trace_id + slide_id
    print(get_context())     # {'trace_id': '01J9Z8...', 'slide_id': 'TCGA-AA-1234'}
```

`new_trace_id()` returns a 26-character, time-sortable ID. `bind_context` accepts any field name — `trace_id`, `run_id`, `slide_id`, and `stage` are the ones the SDK's own pipeline sets and surface in the format table above, but you can bind your own (for example `batch_id`) the same way.

## Read your logs back in Python

Read the log file without parsing JSON yourself. Both readers anonymize by default — pass `anonymize=False` for full local detail:

```python theme={null}
from bioptimus.observability import read_recent_logs, read_usage_records

recent = read_recent_logs(max_entries=50)  # last N entries of the active file, oldest first
usage = read_usage_records()               # every model-request record, across rotations
```

Each entry is a plain `dict` with the fields shown in [Default log format](#default-log-format). `read_recent_logs` tails the active file only; `read_usage_records` spans the active file and all its rotations, so it is the full request history rather than a recent tail.

## Customize logging

Call `configure_logging` to override any default — for a custom path, console output, level, or retention:

```python theme={null}
from pathlib import Path

from bioptimus.observability import configure_logging

configure_logging(console=True, level="DEBUG", log_file=Path("~/.bioptimus/logs/bioptimus.log").expanduser())
```

With `console=True`, matching events also print a compact, human-readable line to `stderr` instead of the JSON payload above — for example the `slide_skipped` event from the manual `log_event` call:

```text theme={null}
12:00:00 WARNING slide_skipped   reason=tissue_mask_empty   slide_id=s1234
```

| Parameter              | Purpose                                                   | Default                                |
| ---------------------- | --------------------------------------------------------- | -------------------------------------- |
| `level`                | Minimum severity to emit                                  | `logging.INFO`                         |
| `log_file`             | Explicit file path, overrides `BIOPTIMUS_LOG_FILE`        | The env var, else `default_log_file()` |
| `console`              | Also print human-readable lines to `stderr`               | `False`                                |
| `stream`               | Custom stream for console output (implies `console=True`) | `None`                                 |
| `anonymize`            | Redact PII at rest, in the file itself                    | `False`                                |
| `when`                 | Rotation interval (`"midnight"`, `"H"`, `"D"`, ...)       | `"midnight"`                           |
| `backup_count`         | Number of rotated files kept                              | `14`                                   |
| `file_handler_factory` | Swap in a different handler, e.g. size-based rotation     | `None`                                 |
| `logger_name`          | Logger to configure (`None` targets the root logger)      | `"bioptimus"`                          |

**Set the rotation period and retention** with `when` and `backup_count`. Rotated files are suffixed with their UTC date (`bioptimus.log.2026-06-24`), so each maps cleanly to a support window:

```python theme={null}
configure_logging(when="H", backup_count=48)   # rotate hourly, keep two days
configure_logging(backup_count=0)               # keep every rotated file
```

For a policy those two parameters can't express — size-based rotation, say — pass your own handler through `file_handler_factory`. The SDK still owns the path, the JSON format, and correlation-context injection:

```python theme={null}
from logging.handlers import RotatingFileHandler

configure_logging(
    file_handler_factory=lambda path: RotatingFileHandler(
        path, maxBytes=50_000_000, backupCount=5, encoding="utf-8"
    ),
)
```

### Can I change the log format?

The on-disk format is **structured JSON by design** — it is the contract the [readers](#read-your-logs-back-in-python), [support bundles](/guides/get-started/support-bundles), and a server's `/diagnostics` all parse — so there is no `format` parameter. For human-readable output, pass `console=True` (shown above).

To emit SDK logs in your own format to your own destination, attach your own handler. SDK records propagate up Python's logging hierarchy, so a handler on the `bioptimus` logger (or the root logger) receives every event:

```python theme={null}
import logging

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logging.getLogger("bioptimus").addHandler(handler)
```

## Redact sensitive fields at rest

By default the log file keeps **full detail** so you can debug locally, and PII is redacted only when you **export** — when you build a support bundle or query a server's `/diagnostics`. To redact the on-disk file itself, turn on redaction at rest:

```python theme={null}
from bioptimus.observability import configure_logging

configure_logging(anonymize=True)  # redact PII in the file, not only on export
```

With redaction on, every emitted record is scrubbed before it is written:

* **Identifier and path fields** (`slide_id`, `patient_id`, `wsi_id`, `slide_path`, `output_dir`, ...) are replaced by a **stable hash** — the same value keeps the same pseudonym, so *"which slide failed"* still correlates across every line.
* **Free text** — any path, `s3://`/`file://` URI, or slide file name embedded in a message or traceback under a field the policy doesn't recognize — is redacted outright instead; the user name in a home-directory path is masked.

The recognized fields and the per-field strategy (hash, redact, or keep verbatim) are configurable — see [Support bundles → Customize what gets redacted](/guides/get-started/support-bundles#customize-what-gets-redacted) for the full policy and the [`anonymize` reference](/sdk-reference/observability/anonymize).

<Note>
  Redaction is stable pseudonymization for support triage, not encryption. Review [what a bundle contains](/guides/get-started/support-bundles#what-gets-shared) before sharing logs outside your environment.
</Note>

## Handle errors in your code

Every failure the SDK raises is a `BioptimusError` carrying a machine-readable `code`, structured `context`, and an actionable `remediation`. Catch the base class to handle any SDK failure and read those fields directly:

```python theme={null}
from bioptimus.observability import BioptimusError, ErrorCode

try:
    inference.predict("slide.svs")
except BioptimusError as err:
    print(err.code)          # ErrorCode.WSI_OPEN_FAILED
    print(err.code.value)    # "BIO-WSI-002"  — the stable support code
    print(err.context)       # {"path": "slide.svs", "errno": 2, ...}
    print(err.remediation)   # "Verify the slide path points to a valid, ..."
    record = err.to_dict()   # JSON-friendly dict for your own logging / telemetry

    if err.code is ErrorCode.WSI_UNSUPPORTED_FORMAT:
        ...                  # branch on a specific failure class
```

**Existing `except` handlers keep working.** Each typed error subclasses the built-in it replaces, so code that already catches the standard exception is unaffected:

| Typed error             | Also caught by        | Raised for                                                            |
| ----------------------- | --------------------- | --------------------------------------------------------------------- |
| `BioptimusValueError`   | `except ValueError`   | Invalid input — bad path, unsupported format, malformed file          |
| `BioptimusRuntimeError` | `except RuntimeError` | Invalid state — backend unavailable, model/forward failure, disk full |
| `BioptimusImportError`  | `except ImportError`  | A feature needs an optional extra that is not installed               |

`BioptimusImportError` names the exact extra to install, so a missing optional dependency is self-explanatory:

```python theme={null}
from bioptimus.observability.errors import BioptimusImportError

try:
    ...
except BioptimusImportError as err:
    print(err.remediation)   # e.g. "Install ... pip install bioptimus-sdk[torch] ..."
```

## Error codes

Every `BioptimusError` carries a stable, hyphen-delimited code such as `BIO-WSI-002`, grouped by domain:

| Domain                       | Prefix          | Covers                                             |
| ---------------------------- | --------------- | -------------------------------------------------- |
| Whole-slide image reading    | `BIO-WSI-xxx`   | Opening/reading slide files, missing backends      |
| Tissue masking               | `BIO-MASK-xxx`  | Tissue mask generation and validation              |
| Tile extraction              | `BIO-TILE-xxx`  | Extracting tiles from a slide                      |
| Bulk-RNA / omics input       | `BIO-RNA-xxx`   | Omics file schema and content                      |
| Request dispatch / transport | `BIO-NET-xxx`   | Talking to a remote or AWS endpoint                |
| Model execution              | `BIO-MODEL-xxx` | Loading or running a model                         |
| Output writing               | `BIO-WRITE-xxx` | Writing inference outputs                          |
| Slide-level aggregation      | `BIO-AGG-xxx`   | Combining tile-level results                       |
| Environment / runtime        | `BIO-ENV-xxx`   | Disk space, GPU availability, missing dependencies |
| Configuration                | `BIO-CFG-xxx`   | Invalid SDK configuration                          |
| Unclassified                 | `BIO-GEN-xxx`   | Fallback for anything else                         |

Each code has a fixed remediation string, the same one attached to its `error_code` field above and surfaced by [`verify_endpoint`](/guides/get-started/support-bundles#verify-an-endpoint)'s failure reports. The full list of codes and remediations is in the [API reference](/sdk-reference/observability/errors).
