Source code for endgame.tracking.console_logger

from __future__ import annotations

"""Lightweight console/file logger with no external dependencies."""

import json
import logging
import time
from pathlib import Path
from typing import Any

from endgame.tracking.base import ExperimentLogger

logger = logging.getLogger(__name__)


[docs] class ConsoleLogger(ExperimentLogger): """Simple console/file logger with no external dependencies. Prints experiment tracking information to the console and optionally writes a JSON log file. Useful for lightweight tracking without MLflow. Parameters ---------- log_file : str or Path, optional Path to a JSON log file. If provided, all events are appended. verbose : bool, default=True Whether to print to console. Examples -------- >>> logger = ConsoleLogger() >>> with logger: ... logger.log_params({"lr": 0.01, "epochs": 10}) ... logger.log_metrics({"accuracy": 0.95}) With file logging: >>> logger = ConsoleLogger(log_file="experiment_log.json") >>> logger.start_run("my_experiment") >>> logger.log_metrics({"f1": 0.92}) >>> logger.end_run() """ def __init__( self, log_file: str | Path | None = None, verbose: bool = True, ): self.log_file = Path(log_file) if log_file else None self.verbose = verbose self._run_id: str | None = None self._run_name: str | None = None self._start_time: float | None = None self._params: dict[str, Any] = {} self._metrics: list[dict[str, Any]] = [] self._artifacts: list[str] = [] self._experiment_name: str = "default"
[docs] def start_run( self, run_name: str | None = None, tags: dict[str, str] | None = None, ) -> str: """Start a new run.""" self._run_id = f"console-{int(time.time() * 1000)}" self._run_name = run_name or self._run_id self._start_time = time.time() self._params = {} self._metrics = [] self._artifacts = [] if self.verbose: print(f"[Tracking] Run started: {self._run_name}") if tags: print(f"[Tracking] Tags: {tags}") return self._run_id
[docs] def end_run(self, status: str = "FINISHED") -> None: """End the current run.""" duration = time.time() - self._start_time if self._start_time else 0 if self.verbose: print(f"[Tracking] Run ended: {self._run_name} ({status}, {duration:.1f}s)") # Write to log file if configured if self.log_file: entry = { "run_id": self._run_id, "run_name": self._run_name, "experiment": self._experiment_name, "status": status, "duration": round(duration, 2), "params": self._params, "metrics": self._metrics, "artifacts": self._artifacts, } self.log_file.parent.mkdir(parents=True, exist_ok=True) # Append to JSON lines file with open(self.log_file, "a") as f: f.write(json.dumps(entry) + "\n") self._run_id = None self._start_time = None
[docs] def log_params(self, params: dict[str, Any]) -> None: """Log parameters.""" self._params.update(params) if self.verbose: for k, v in params.items(): print(f"[Tracking] param {k}={v}")
[docs] def log_metrics(self, metrics: dict[str, float], step: int | None = None) -> None: """Log metrics.""" entry = {**metrics} if step is not None: entry["_step"] = step self._metrics.append(entry) if self.verbose: step_str = f" (step={step})" if step is not None else "" for k, v in metrics.items(): print(f"[Tracking] metric {k}={v:.4f}{step_str}")
[docs] def log_artifact(self, local_path: str, artifact_path: str | None = None) -> None: """Log an artifact path.""" self._artifacts.append(local_path) if self.verbose: dest = f" -> {artifact_path}" if artifact_path else "" print(f"[Tracking] artifact {local_path}{dest}")
[docs] def log_model(self, model: Any, artifact_path: str = "model", **kwargs) -> None: """Log a model (records type name only).""" model_type = type(model).__name__ self._artifacts.append(f"{artifact_path}/{model_type}") if self.verbose: print(f"[Tracking] model {model_type} -> {artifact_path}")
[docs] def set_experiment(self, name: str) -> None: """Set the active experiment name.""" self._experiment_name = name if self.verbose: print(f"[Tracking] Experiment: {name}")
def __repr__(self) -> str: return f"ConsoleLogger(log_file={self.log_file!r}, verbose={self.verbose})"