feat(logging): Implement session-based log organization

This commit is contained in:
2026-02-26 08:55:16 -05:00
parent 10fbfd0f54
commit 3f4dc1ae03
4 changed files with 94 additions and 18 deletions

View File

@@ -263,7 +263,8 @@ class App:
self._autosave_interval = 60.0 self._autosave_interval = 60.0
self._last_autosave = time.time() self._last_autosave = time.time()
session_logger.open_session() label = self.project.get("project", {}).get("name", "")
session_logger.open_session(label=label)
self._init_ai_and_hooks() self._init_ai_and_hooks()
@property @property

View File

@@ -513,7 +513,8 @@ class App:
"cache_creation_input_tokens": 0 "cache_creation_input_tokens": 0
} }
session_logger.open_session() label = self.project.get("project", {}).get("name", "")
session_logger.open_session(label=label)
ai_client.set_provider(self.current_provider, self.current_model) ai_client.set_provider(self.current_provider, self.current_model)
ai_client.confirm_and_run_callback = self._confirm_and_run ai_client.confirm_and_run_callback = self._confirm_and_run
ai_client.comms_log_callback = self._on_comms_entry ai_client.comms_log_callback = self._on_comms_entry

View File

@@ -26,46 +26,62 @@ _LOG_DIR = Path("./logs")
_SCRIPTS_DIR = Path("./scripts/generated") _SCRIPTS_DIR = Path("./scripts/generated")
_ts: str = "" # session timestamp string e.g. "20260301_142233" _ts: str = "" # session timestamp string e.g. "20260301_142233"
_session_id: str = "" # YYYYMMDD_HHMMSS[_Label]
_session_dir: Path = None # Path to the sub-directory for this session
_seq: int = 0 # monotonic counter for script files this session _seq: int = 0 # monotonic counter for script files this session
_seq_lock = threading.Lock() _seq_lock = threading.Lock()
_comms_fh = None # file handle: logs/comms_<ts>.log _comms_fh = None # file handle: logs/<session_id>/comms.log
_tool_fh = None # file handle: logs/toolcalls_<ts>.log _tool_fh = None # file handle: logs/<session_id>/toolcalls.log
_api_fh = None # file handle: logs/apihooks_<ts>.log - API hook calls _api_fh = None # file handle: logs/<session_id>/apihooks.log
_cli_fh = None # file handle: logs/clicalls_<ts>.log - CLI subprocess calls _cli_fh = None # file handle: logs/<session_id>/clicalls.log
def _now_ts() -> str: def _now_ts() -> str:
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S") return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
def open_session(): def open_session(label: str | None = None):
""" """
Called once at GUI startup. Creates the log directories if needed and Called once at GUI startup. Creates the log directories if needed and
opens the two log files for this session. Idempotent - a second call is opens the log files for this session within a sub-directory.
ignored.
""" """
global _ts, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq
if _comms_fh is not None: if _comms_fh is not None:
return # already open return # already open
_LOG_DIR.mkdir(parents=True, exist_ok=True) _ts = _now_ts()
_session_id = _ts
if label:
# Sanitize label: remove non-alphanumeric chars
safe_label = "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in label)
_session_id += f"_{safe_label}"
_session_dir = _LOG_DIR / _session_id
_session_dir.mkdir(parents=True, exist_ok=True)
_SCRIPTS_DIR.mkdir(parents=True, exist_ok=True) _SCRIPTS_DIR.mkdir(parents=True, exist_ok=True)
_ts = _now_ts()
_seq = 0 _seq = 0
_comms_fh = open(_LOG_DIR / f"comms_{_ts}.log", "w", encoding="utf-8", buffering=1) _comms_fh = open(_session_dir / "comms.log", "w", encoding="utf-8", buffering=1)
_tool_fh = open(_LOG_DIR / f"toolcalls_{_ts}.log", "w", encoding="utf-8", buffering=1) _tool_fh = open(_session_dir / "toolcalls.log", "w", encoding="utf-8", buffering=1)
_api_fh = open(_LOG_DIR / f"apihooks_{_ts}.log", "w", encoding="utf-8", buffering=1) _api_fh = open(_session_dir / "apihooks.log", "w", encoding="utf-8", buffering=1)
_cli_fh = open(_LOG_DIR / f"clicalls_{_ts}.log", "w", encoding="utf-8", buffering=1) # New log file handle _cli_fh = open(_session_dir / "clicalls.log", "w", encoding="utf-8", buffering=1)
_tool_fh.write(f"# Tool-call log — session {_ts}\n\n") _tool_fh.write(f"# Tool-call log — session {_session_id}\n\n")
_tool_fh.flush() _tool_fh.flush()
_cli_fh.write(f"# CLI Subprocess Call Log — session {_ts}\n\n") # Header for new log file _cli_fh.write(f"# CLI Subprocess Call Log — session {_session_id}\n\n")
_cli_fh.flush() _cli_fh.flush()
# Register this session in the log registry
try:
from log_registry import LogRegistry
registry = LogRegistry(str(_LOG_DIR / "log_registry.toml"))
registry.register_session(_session_id, str(_session_dir), datetime.datetime.now())
except Exception as e:
print(f"Warning: Could not register session in LogRegistry: {e}")
atexit.register(close_session) atexit.register(close_session)

View File

@@ -0,0 +1,58 @@
import os
import shutil
import pytest
from pathlib import Path
from datetime import datetime
from unittest.mock import patch
import session_logger
import tomllib
@pytest.fixture
def temp_logs(tmp_path):
# Mock _LOG_DIR in session_logger
original_log_dir = session_logger._LOG_DIR
session_logger._LOG_DIR = tmp_path / "logs"
session_logger._LOG_DIR.mkdir(parents=True, exist_ok=True)
# Mock _SCRIPTS_DIR
original_scripts_dir = session_logger._SCRIPTS_DIR
session_logger._SCRIPTS_DIR = tmp_path / "scripts" / "generated"
session_logger._SCRIPTS_DIR.mkdir(parents=True, exist_ok=True)
yield tmp_path / "logs"
# Cleanup: Close handles if open
session_logger.close_session()
session_logger._LOG_DIR = original_log_dir
session_logger._SCRIPTS_DIR = original_scripts_dir
def test_open_session_creates_subdir_and_registry(temp_logs):
label = "test-label"
# We can't easily mock datetime.datetime.now() because it's a built-in
# but we can check the resulting directory name pattern
session_logger.open_session(label=label)
# Check that a subdirectory was created
subdirs = list(temp_logs.iterdir())
# One is the log_registry.toml, one is the session dir
session_dirs = [d for d in subdirs if d.is_dir()]
assert len(session_dirs) == 1
session_dir = session_dirs[0]
assert session_dir.name.endswith(f"_{label}")
# Check for log files
assert (session_dir / "comms.log").exists()
assert (session_dir / "toolcalls.log").exists()
assert (session_dir / "apihooks.log").exists()
assert (session_dir / "clicalls.log").exists()
# Check registry
registry_path = temp_logs / "log_registry.toml"
assert registry_path.exists()
with open(registry_path, "rb") as f:
data = tomllib.load(f)
assert session_dir.name in data
assert data[session_dir.name]["path"] == str(session_dir)