Source code for molecular_simulations.simulate.constantph.logging

import json
import logging
from datetime import datetime, timezone
from pathlib import Path


[docs] def setup_task_logger(run_id: str, task_id: str, log_dir: str) -> logging.Logger: """Create a logger for a specific task with deterministic path.""" # Hierarchical structure: logs/{run_id}/{task_id_prefix}/{task_id}.jsonl # The prefix bucketing prevents directory explosion prefix = task_id[:3] if len(task_id) >= 3 else task_id task_log_dir = Path(log_dir) / run_id / prefix task_log_dir.mkdir(parents=True, exist_ok=True) log_path = task_log_dir / f'{task_id}.jsonl' logger = logging.getLogger(f'task.{task_id}') logger.setLevel(logging.DEBUG) logger.handlers.clear() handler = logging.FileHandler(log_path) handler.setFormatter(JsonFormatter(task_id=task_id, run_id=run_id)) logger.addHandler(handler) return logger
[docs] class JsonFormatter(logging.Formatter): """Structured JSON logging for easy aggregation."""
[docs] def __init__(self, task_id: str, run_id: str): super().__init__() self.task_id = task_id self.run_id = run_id
[docs] def format(self, record) -> str: entry = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'run_id': self.run_id, 'task_id': self.task_id, 'level': record.levelname, 'message': record.getMessage(), } # Merge in any extra fields passed via extra={} for key, val in record.__dict__.items(): if key not in ( 'msg', 'args', 'levelname', 'levelno', 'pathname', 'filename', 'module', 'lineno', 'funcName', 'created', 'msecs', 'relativeCreated', 'thread', 'threadName', 'processName', 'process', 'message', 'exc_info', 'exc_text', 'stack_info', 'name', ): entry[key] = val return json.dumps(entry)