diff --git a/api/admin_config.py b/api/admin_config.py index edc7f40..cb4f7ab 100644 --- a/api/admin_config.py +++ b/api/admin_config.py @@ -413,14 +413,6 @@ FIELDS: tuple[ConfigFieldSpec, ...] = ( default="8082", restart_required=True, ), - ConfigFieldSpec( - "LOG_FILE", - "Log File", - "runtime", - settings_attr="log_file", - default="logs/server.log", - restart_required=True, - ), ConfigFieldSpec( "MESSAGING_PLATFORM", "Messaging Platform", diff --git a/api/app.py b/api/app.py index 51f0406..05f5519 100644 --- a/api/app.py +++ b/api/app.py @@ -12,6 +12,7 @@ from loguru import logger from starlette.types import Receive, Scope, Send from config.logging_config import configure_logging +from config.paths import server_log_path from config.settings import get_settings from core.trace import extract_claude_session_id_from_headers, trace_event from providers.exceptions import ProviderError @@ -85,7 +86,7 @@ def create_app(*, lifespan_enabled: bool = True) -> FastAPI: """Create and configure the FastAPI application.""" settings = get_settings() configure_logging( - settings.log_file, verbose_third_party=settings.log_raw_api_payloads + server_log_path(), verbose_third_party=settings.log_raw_api_payloads ) app_kwargs: dict[str, Any] = { diff --git a/config/logging_config.py b/config/logging_config.py index 1f09bb0..dd100fa 100644 --- a/config/logging_config.py +++ b/config/logging_config.py @@ -105,7 +105,7 @@ class InterceptHandler(logging.Handler): def configure_logging( - log_file: str, *, force: bool = False, verbose_third_party: bool = False + log_file: str | Path, *, force: bool = False, verbose_third_party: bool = False ) -> None: """Configure loguru with JSON output to log_file and intercept stdlib logging. diff --git a/config/paths.py b/config/paths.py index 0ab4e36..75bae73 100644 --- a/config/paths.py +++ b/config/paths.py @@ -5,6 +5,8 @@ from pathlib import Path FCC_CONFIG_DIRNAME = ".fcc" FCC_ENV_FILENAME = ".env" CLAUDE_WORKSPACE_DIRNAME = "agent_workspace" +FCC_LOGS_DIRNAME = "logs" +SERVER_LOG_FILENAME = "server.log" def config_dir_path() -> Path: @@ -23,3 +25,9 @@ def default_claude_workspace_path() -> Path: """Return the default Claude workspace path.""" return config_dir_path() / CLAUDE_WORKSPACE_DIRNAME + + +def server_log_path() -> Path: + """Return the canonical server log path.""" + + return config_dir_path() / FCC_LOGS_DIRNAME / SERVER_LOG_FILENAME diff --git a/config/settings.py b/config/settings.py index 15f9f45..e451761 100644 --- a/config/settings.py +++ b/config/settings.py @@ -310,7 +310,6 @@ class Settings(BaseSettings): # ==================== Server ==================== host: str = "0.0.0.0" port: int = 8082 - log_file: str = "logs/server.log" # Optional server API key to protect endpoints (Anthropic-style) # Set via env `ANTHROPIC_AUTH_TOKEN`. When empty, no auth is required. anthropic_auth_token: str = Field( diff --git a/tests/api/test_admin.py b/tests/api/test_admin.py index 80f1064..89d3e74 100644 --- a/tests/api/test_admin.py +++ b/tests/api/test_admin.py @@ -57,6 +57,7 @@ def test_admin_config_masks_secrets_and_exposes_manifest(monkeypatch, tmp_path): keys = {field["key"] for field in body["fields"]} assert "ANTHROPIC_AUTH_TOKEN" in keys assert "OPENROUTER_API_KEY" in keys + assert "LOG_FILE" not in keys auth_field = next( field for field in body["fields"] if field["key"] == "ANTHROPIC_AUTH_TOKEN" ) diff --git a/tests/api/test_app_lifespan_and_errors.py b/tests/api/test_app_lifespan_and_errors.py index 7f287f0..cbb0a1c 100644 --- a/tests/api/test_app_lifespan_and_errors.py +++ b/tests/api/test_app_lifespan_and_errors.py @@ -85,7 +85,6 @@ def test_create_app_provider_error_handler_returns_anthropic_format(): claude_workspace="./agent_workspace", host="127.0.0.1", port=8082, - log_file="server.log", ) with ( patch.object(api_app_mod, "get_settings", return_value=settings), @@ -122,7 +121,6 @@ def test_create_app_provider_error_default_logs_exclude_provider_message(): claude_workspace="./agent_workspace", host="127.0.0.1", port=8082, - log_file="server.log", log_api_error_tracebacks=False, ) with ( @@ -160,7 +158,6 @@ def test_create_app_general_exception_handler_returns_500(): claude_workspace="./agent_workspace", host="127.0.0.1", port=8082, - log_file="server.log", ) with ( patch.object(api_app_mod, "get_settings", return_value=settings), @@ -197,7 +194,6 @@ def test_create_app_general_exception_default_logs_exclude_exception_message(): claude_workspace="./agent_workspace", host="127.0.0.1", port=8082, - log_file="server.log", log_api_error_tracebacks=False, ) with ( @@ -236,7 +232,6 @@ def test_app_lifespan_sets_state_and_cleans_up(tmp_path, messaging_enabled): claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) fake_platform = MagicMock() @@ -315,7 +310,6 @@ def test_app_lifespan_cleanup_continues_if_platform_stop_raises(tmp_path): claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) fake_platform = MagicMock() @@ -366,7 +360,6 @@ async def test_runtime_startup_validation_failure_does_not_block_server(tmp_path claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) app = FastAPI() runtime = api_runtime_mod.AppRuntime( @@ -414,7 +407,6 @@ async def test_graceful_asgi_lifespan_model_validation_failure_starts(tmp_path): claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) app = api_app_mod.GracefulLifespanApp(FastAPI()) sent: list[MutableMapping[str, Any]] = [] @@ -460,7 +452,6 @@ def test_app_lifespan_messaging_import_error_no_crash(tmp_path, caplog): claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) api_app_mod = importlib.import_module("api.app") @@ -496,7 +487,6 @@ def test_app_lifespan_platform_start_exception_cleanup_still_runs(tmp_path): claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) fake_platform = MagicMock() @@ -547,7 +537,6 @@ def test_app_lifespan_flush_pending_save_exception_warning_only(tmp_path): claude_workspace=str(tmp_path / "data"), host="127.0.0.1", port=8082, - log_file=str(tmp_path / "server.log"), ) fake_platform = MagicMock() @@ -582,3 +571,29 @@ def test_app_lifespan_flush_pending_save_exception_warning_only(tmp_path): session_store.flush_pending_save.assert_called_once() registry_cleanup.assert_awaited_once() + + +def test_create_app_writes_server_log_under_fcc_home(monkeypatch, tmp_path): + """App logging uses ~/.fcc/logs/server.log regardless of cwd.""" + from loguru import logger + + import config.logging_config as logging_config_mod + from api.app import create_app + from config.paths import server_log_path + + run_dir = tmp_path / "run" + run_dir.mkdir() + monkeypatch.chdir(run_dir) + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + monkeypatch.setattr(logging_config_mod, "_configured", False) + + create_app(lifespan_enabled=False) + logger.info("canonical log path test") + logger.complete() + + canonical_log = server_log_path() + assert canonical_log == tmp_path / ".fcc" / "logs" / "server.log" + assert canonical_log.is_file() + assert "canonical log path test" in canonical_log.read_text(encoding="utf-8") + assert not (run_dir / "logs" / "server.log").exists() diff --git a/tests/api/test_runtime_safe_logging.py b/tests/api/test_runtime_safe_logging.py index 5c86c15..8b13147 100644 --- a/tests/api/test_runtime_safe_logging.py +++ b/tests/api/test_runtime_safe_logging.py @@ -22,7 +22,6 @@ async def test_messaging_start_failure_default_logs_exclude_traceback(caplog): claude_workspace="./agent_workspace", host="127.0.0.1", port=8082, - log_file="server.log", log_api_error_tracebacks=False, ) runtime = api_runtime_mod.AppRuntime(app=MagicMock(), settings=settings) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index c71002f..86549d3 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -57,6 +57,26 @@ class TestSettings: assert settings.claude_workspace == str(tmp_path / ".fcc" / "agent_workspace") + def test_server_log_path_uses_fcc_home(self, monkeypatch, tmp_path): + """The server log location is fixed under ~/.fcc.""" + from config.paths import server_log_path + + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + + assert server_log_path() == tmp_path / ".fcc" / "logs" / "server.log" + + def test_removed_log_file_env_is_ignored(self, monkeypatch): + """Legacy LOG_FILE values do not affect Settings or block startup.""" + from config.settings import Settings + + monkeypatch.setenv("LOG_FILE", "custom/server.log") + monkeypatch.setitem(Settings.model_config, "env_file", ()) + + settings = Settings() + + assert not hasattr(settings, "log_file") + def test_blank_claude_workspace_uses_fcc_home(self, monkeypatch, tmp_path): """An explicit blank env value keeps the default workspace path.""" from config.settings import Settings