Update logging configuration to support daily log files and implement log pruning; add guidance in CLAUDE.md and enhance README.md with command usage
This commit is contained in:
@@ -1,23 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
import nexus_claude_api.config as config
|
||||
|
||||
LOCAL_LOG_DIR = Path("logs")
|
||||
LOCAL_LOG_FILE = LOCAL_LOG_DIR / "nexus-claude-api.log"
|
||||
LOG_FILE_PREFIX = "nexus-claude-api"
|
||||
LOG_FILE_SUFFIX = ".log"
|
||||
LOG_RETENTION_DAYS = 7
|
||||
LOCAL_LOG_FILE = LOCAL_LOG_DIR / f"{LOG_FILE_PREFIX}.log"
|
||||
USER_LOG_DIR = config.USER_CONFIG_DIR / "logs"
|
||||
USER_LOG_FILE = USER_LOG_DIR / "nexus-claude-api.log"
|
||||
USER_LOG_FILE = USER_LOG_DIR / f"{LOG_FILE_PREFIX}.log"
|
||||
LOG_FILE = USER_LOG_FILE
|
||||
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
|
||||
|
||||
def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Path:
|
||||
DateProvider = Callable[[float | None], datetime]
|
||||
|
||||
|
||||
def _local_datetime(timestamp: float | None = None) -> datetime:
|
||||
if timestamp is None:
|
||||
return datetime.fromtimestamp(time.time())
|
||||
return datetime.fromtimestamp(timestamp)
|
||||
|
||||
|
||||
def _dated_log_file(log_dir: Path, current_date: datetime) -> Path:
|
||||
return log_dir / f"{LOG_FILE_PREFIX}-{current_date:%Y-%m-%d}{LOG_FILE_SUFFIX}"
|
||||
|
||||
|
||||
class DailyLogFileHandler(logging.Handler):
|
||||
def __init__(
|
||||
self,
|
||||
log_file: Path,
|
||||
*,
|
||||
date_provider: DateProvider = _local_datetime,
|
||||
retention_days: int = LOG_RETENTION_DAYS,
|
||||
encoding: str = "utf-8",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.log_dir = log_file.parent
|
||||
self.date_provider = date_provider
|
||||
self.retention_days = retention_days
|
||||
self.encoding = encoding
|
||||
self.current_date = ""
|
||||
self.stream = None
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._open_for_date(self.date_provider(None))
|
||||
self._prune_old_logs()
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
try:
|
||||
record_datetime = self.date_provider(record.created)
|
||||
self._open_for_date(record_datetime)
|
||||
message = self.format(record)
|
||||
if self.stream is None:
|
||||
return
|
||||
self.stream.write(message + self.terminator)
|
||||
self.flush()
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
@property
|
||||
def baseFilename(self) -> str:
|
||||
return str(self.current_log_file)
|
||||
|
||||
@property
|
||||
def terminator(self) -> str:
|
||||
return "\n"
|
||||
|
||||
def flush(self) -> None:
|
||||
if self.stream and not self.stream.closed:
|
||||
self.stream.flush()
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
if self.stream and not self.stream.closed:
|
||||
self.stream.close()
|
||||
finally:
|
||||
self.stream = None
|
||||
super().close()
|
||||
|
||||
def _open_for_date(self, current_datetime: datetime) -> None:
|
||||
date_text = current_datetime.strftime("%Y-%m-%d")
|
||||
if date_text == self.current_date and self.stream is not None:
|
||||
return
|
||||
|
||||
if self.stream is not None and not self.stream.closed:
|
||||
self.stream.close()
|
||||
|
||||
self.current_date = date_text
|
||||
self.current_log_file = _dated_log_file(self.log_dir, current_datetime)
|
||||
self.stream = self.current_log_file.open("a", encoding=self.encoding)
|
||||
self._prune_old_logs()
|
||||
|
||||
def _prune_old_logs(self) -> None:
|
||||
dated_logs = []
|
||||
for path in self.log_dir.glob(f"{LOG_FILE_PREFIX}-*{LOG_FILE_SUFFIX}"):
|
||||
date_text = path.stem.removeprefix(f"{LOG_FILE_PREFIX}-")
|
||||
try:
|
||||
log_date = datetime.strptime(date_text, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
continue
|
||||
dated_logs.append((log_date, path))
|
||||
|
||||
dated_logs.sort(reverse=True)
|
||||
for _, path in dated_logs[self.retention_days :]:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def configure_logging(
|
||||
*,
|
||||
verbose: bool = False,
|
||||
log_file: Path = LOG_FILE,
|
||||
date_provider: DateProvider = _local_datetime,
|
||||
) -> Path:
|
||||
log_file = _dated_log_file(log_file.parent, date_provider(None))
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
formatter = logging.Formatter(LOG_FORMAT)
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler = DailyLogFileHandler(log_file, date_provider=date_provider)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
@@ -32,5 +137,5 @@ def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Pa
|
||||
|
||||
def resolve_log_file(*, dev: bool = False) -> Path:
|
||||
if dev:
|
||||
return LOCAL_LOG_FILE
|
||||
return config.USER_CONFIG_DIR / "logs" / "nexus-claude-api.log"
|
||||
return _dated_log_file(LOCAL_LOG_DIR, _local_datetime(None))
|
||||
return _dated_log_file(config.USER_CONFIG_DIR / "logs", _local_datetime(None))
|
||||
|
||||
Reference in New Issue
Block a user