Skip to content

Logging

RL Engine's shared logger lives in src/rle/reusable/log and is exported from rle.reusable. It is intended for notebooks, scripts, and small experiment runners that need consistent console output plus simple append-only log files.

As a reusable component, logging is meant to be lifted into a new research repo early. It gives notebooks, quick scripts, and batch jobs a single log(...) surface without requiring a logging configuration module on day one.

Quick Start

from rle.reusable import DEBUG, configure_logging, log

configure_logging(min_level=DEBUG, logs_dir="logs")

log("training started")
log.debug({"episode": 1, "reward": 3.0})
log.warning("constraint violation", type="safety")

If you skip configuration, the first log call lazily enables console output and file output to logs/app.log.

Public API

Use log(message) for an INFO record:

log("rollout complete")

Use level methods when the level matters at the call site:

log.trace("sample detail")
log.debug("created batch")
log.info("started run")
log.success("checkpoint written")
log.warning("constraint violation")
log.error("rollout failed")
log.critical("cannot continue")

Each call accepts:

argument purpose
message object to render as the log payload
type optional stream name routed to <type>.log
console per-call console sink override
file per-call file sink override
exception True, an exception object, or False/None

configure_logging() controls interpreter-wide defaults:

configure_logging(
    min_level="INFO",
    logs_dir="logs",
    console_enabled=True,
    file_enabled=True,
    log_format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {name}:{function}:{line} | {message}",
)

File Routing

Untyped logs go to app.log:

log("episode complete")

Typed logs go to their own file only:

log("opened sqlite connection", type="db")
log.warning("unsafe state", type="safety")

With the default logs_dir, those examples append to:

logs/app.log
logs/db.log
logs/safety.log

Typed records still print to the console when console logging is enabled, but they do not also write to app.log.

Sink Overrides

Global sink switches are set by configure_logging():

configure_logging(console_enabled=True, file_enabled=True)

Individual calls can opt out of either sink:

log("persist this without printing", console=False)
log("print this without persisting", file=False)

This is useful in notebooks where some records should be kept in the file log without cluttering visible cell output.

Exceptions

Inside an exception handler, pass exception=True to include the active traceback:

try:
    run_rollout()
except RuntimeError:
    log.error("rollout failed", exception=True)

Passing the exception object is also supported:

try:
    run_rollout()
except RuntimeError as exc:
    log.error("rollout failed", exception=exc)

Concurrency

File output uses a callback sink rather than a persistent loguru file handler. For each record the logger resolves the target file, acquires a POSIX flock through rle.internal.locks.exclusive_file_lock, appends one rendered line, flushes, and closes.

This makes multi-notebook and multi-process appends predictable. It is optimized for experiment logging and debugging, not for very high-volume production event streams. If a file lock times out, the file append is skipped and a direct warning is written to the configured console sink or sys.stderr.

Copying To Another Project

Copy src/rle/reusable/log and src/rle/internal/locks.py, then add loguru to the new project's dependencies. If the package name changes, update imports from rle.reusable and rle.internal to the new package path.

The component has no dependency on RL Engine environments or experiment code. The default log directory is resolved from the nearest pyproject.toml, which works well for standalone project repos.

Adapting To Your Project

For a new repo, copy src/rle/reusable/log and update the import path exposed by the package root. The only runtime dependency to add for this component is loguru; the file lock helper is standard-library POSIX code that should be copied with the module. Nothing in this component depends on trainers, environments, notebooks, or the experiment runner, so it is usually the easiest reusable module to port.

The main project-level decision is where logs should live. The default resolves paths relative to the nearest pyproject.toml, which is useful for notebooks started from different working directories. If the project has a stronger layout, configure it once in an early notebook or script cell:

from my_project.reusable import configure_logging

configure_logging(logs_dir="exports/logs", min_level="INFO")

The blocks most likely to change are:

  • the package-root re-export, so project code can import log from my_project.reusable;
  • the default logs_dir used by notebooks or batch entry points;
  • the log_format if the project wants shorter scheduler output or more source location detail;
  • stream names passed as type=..., such as type="train", type="eval", type="safety", or type="checkpoint".

Avoid adding experiment-specific routing to the logger itself. If a project wants one log file per environment, algorithm, or run, keep that convention at the call site:

log("checkpoint written", type=f"{env_name}_{algorithm_slug}")

Use console=False for noisy records that should only be persisted, and file=False for interactive notebook messages that should not become durable experiment evidence. In multi-process jobs, keep the file-locking sink intact; it is the part that makes concurrent notebook and scheduler appends predictable.