Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ The backend architecture:
- [Installation](./docs/user_guide/install.md)
- [Getting Started](./docs/user_guide/quick_start.md)

## Logs

Parallax mirrors every log message to both the terminal and a rotating log file stored at `logs/parallax.log` inside your project directory (created automatically). Override the location via `PARALLAX_LOG_DIR` or `PARALLAX_LOG_FILE`, and tweak rotation with `PARALLAX_LOG_MAX_BYTES` and `PARALLAX_LOG_BACKUP_COUNT`.

## Contributing

We warmly welcome contributions of all kinds! For guidelines on how to get involved, please refer to our [Contributing Guide](./docs/CONTRIBUTING.md).
Expand Down
78 changes: 75 additions & 3 deletions src/parallax_utils/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@
import os
import sys
import threading
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional

from parallax_utils.file_util import get_project_root

__all__ = ["get_logger", "use_parallax_log_handler", "set_log_level"]

_init_lock = threading.Lock()
_default_handler: logging.Handler | None = None
_file_handler: logging.Handler | None = None

DEFAULT_LOG_DIR_NAME = "logs"
DEFAULT_LOG_FILE_NAME = "parallax.log"
LOG_FILE_ENV = "PARALLAX_LOG_FILE"
LOG_DIR_ENV = "PARALLAX_LOG_DIR"
LOG_MAX_BYTES_ENV = "PARALLAX_LOG_MAX_BYTES"
LOG_BACKUP_COUNT_ENV = "PARALLAX_LOG_BACKUP_COUNT"
DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024
DEFAULT_LOG_BACKUP_COUNT = 5


class _Ansi:
Expand Down Expand Up @@ -69,12 +83,69 @@ def __init__(self, prefixes):
def filter(self, rec: logging.LogRecord) -> bool:
return any(rec.name.startswith(p) for p in self._prefixes)

_default_handler.addFilter(_ModuleFilter(target_module_prefix))
root.addHandler(_default_handler)
handlers = []
if _default_handler is not None:
handlers.append(_default_handler)
if _file_handler is not None:
handlers.append(_file_handler)

for handler in handlers:
handler.addFilter(_ModuleFilter(target_module_prefix))
root.addHandler(handler)


def _safe_int_from_env(var_name: str, default: int) -> int:
raw_value = os.getenv(var_name)
if raw_value is None:
return default
try:
parsed = int(raw_value)
return parsed if parsed > 0 else default
except (TypeError, ValueError):
return default


def _resolve_log_file_path() -> Path:
env_file = os.getenv(LOG_FILE_ENV)
if env_file:
path = Path(env_file).expanduser()
path.parent.mkdir(parents=True, exist_ok=True)
return path

custom_dir = os.getenv(LOG_DIR_ENV)
if custom_dir:
directory = Path(custom_dir).expanduser()
else:
try:
directory = get_project_root() / DEFAULT_LOG_DIR_NAME
except Exception:
directory = Path.cwd() / DEFAULT_LOG_DIR_NAME
directory.mkdir(parents=True, exist_ok=True)
return directory / DEFAULT_LOG_FILE_NAME


def _create_file_handler(formatter: logging.Formatter) -> logging.Handler | None:
try:
log_path = _resolve_log_file_path()
except Exception:
return None

max_bytes = _safe_int_from_env(LOG_MAX_BYTES_ENV, DEFAULT_LOG_MAX_BYTES)
backup_count = _safe_int_from_env(LOG_BACKUP_COUNT_ENV, DEFAULT_LOG_BACKUP_COUNT)
try:
handler = RotatingFileHandler(
log_path,
maxBytes=max_bytes,
backupCount=backup_count,
)
handler.setFormatter(formatter)
return handler
except Exception:
return None


def _initialize_if_necessary():
global _default_handler
global _default_handler, _file_handler

with _init_lock:
if _default_handler is not None:
Expand All @@ -88,6 +159,7 @@ def _initialize_if_necessary():
formatter = CustomFormatter(fmt=fmt, style="{", datefmt="%b %d %H:%M:%S")
_default_handler = logging.StreamHandler(stream=sys.stdout)
_default_handler.setFormatter(formatter)
_file_handler = _create_file_handler(formatter)

# root level from env or INFO
logging.getLogger().setLevel("INFO")
Expand Down