From ffca3478b7e94e39a29ff869bfc841be3624ce71 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 25 Jul 2025 12:43:37 +0200 Subject: [PATCH] Add file logging --- src/serena/cli.py | 11 +++++++++-- src/serena/config/serena_config.py | 28 +++++++++++++++++++++++++++- src/serena/constants.py | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 444a053..4c40ad5 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -12,7 +12,7 @@ from tqdm import tqdm from serena.agent import SerenaAgent from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode -from serena.config.serena_config import ProjectConfig, SerenaConfig +from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths from serena.constants import ( DEFAULT_CONTEXT, DEFAULT_MODES, @@ -141,15 +141,22 @@ class TopLevelCommands(AutoRegisteringGroup): # initialize logging, using INFO level initially (will later be adjusted by SerenaAgent according to the config) # * memory log handler (for use by GUI/Dashboard) # * stream handler for stderr (for direct console output, which will also be captured by clients like Claude Desktop) + # * file handler # (Note that stdout must never be used for logging, as it is used by the MCP server to communicate with the client.) Logger.root.setLevel(logging.INFO) + formatter = logging.Formatter(SERENA_LOG_FORMAT) memory_log_handler = MemoryLogHandler() Logger.root.addHandler(memory_log_handler) stderr_handler = logging.StreamHandler(stream=sys.stderr) - stderr_handler.formatter = logging.Formatter(SERENA_LOG_FORMAT) + stderr_handler.formatter = formatter Logger.root.addHandler(stderr_handler) + log_path = SerenaPaths().get_next_log_file_path("mcp") + file_handler = logging.FileHandler(log_path, mode="w") + file_handler.formatter = formatter + Logger.root.addHandler(file_handler) log.info("Initializing Serena MCP server") + log.info("Storing logs in %s", log_path) project_file = project_file_arg or project factory = SerenaMCPFactorySingleProcess(context=context, project=project_file, memory_log_handler=memory_log_handler) server = factory.create_mcp_server( diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 29993a1..839f2f4 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -7,6 +7,7 @@ import shutil from collections.abc import Iterable from copy import deepcopy from dataclasses import dataclass, field +from datetime import datetime from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar @@ -14,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar import yaml from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import LogTime +from sensai.util.logging import LogTime, datetime_tag from sensai.util.string import ToStringMixin from serena.constants import ( @@ -30,6 +31,7 @@ from serena.util.inspection import determine_programming_language_composition from solidlsp.ls_config import Language from ..analytics import RegisteredTokenCountEstimator +from ..util.class_decorators import singleton if TYPE_CHECKING: from ..project import Project @@ -39,6 +41,30 @@ T = TypeVar("T") DEFAULT_TOOL_TIMEOUT: float = 240 +@singleton +class SerenaPaths: + """ + Provides paths to various Serena-related directories and files. + """ + + def __init__(self): + self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME + """ + the path to the user's Serena configuration directory, which is typically ~/.serena + """ + + def get_next_log_file_path(self, prefix: str): + """ + :param prefix: the filename prefix indicating the type of the log file + :return: the full path to the log file to use + """ + log_dir = os.path.join(self.user_config_dir, "logs", datetime.now().strftime("%Y-%m-%d")) + os.makedirs(log_dir, exist_ok=True) + return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt") + + # TODO: Paths from constants.py should be moved here + + class ToolSet: def __init__(self, tool_names: set[str]) -> None: self._tool_names = tool_names diff --git a/src/serena/constants.py b/src/serena/constants.py index 4ed24bb..9b0dd43 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -8,6 +8,7 @@ _serena_in_home_managed_dir = Path.home() / ".serena" SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir) +# TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here. REPO_ROOT = str(_repo_root_path) PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates") SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts") @@ -23,7 +24,6 @@ INTERNAL_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "inter SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard") SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons") - DEFAULT_ENCODING = "utf-8" DEFAULT_CONTEXT = "desktop-app" DEFAULT_MODES = ("interactive", "editing")