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
logfrommy_project.reusable; - the default
logs_dirused by notebooks or batch entry points; - the
log_formatif the project wants shorter scheduler output or more source location detail; - stream names passed as
type=..., such astype="train",type="eval",type="safety", ortype="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.