From a6b5d087f8a92c9c7142b621c400da8640dd6806 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 25 Feb 2026 10:39:47 +0800 Subject: [PATCH] fix: scope log filenames by service under root logs dir --- backend/src/core/config/settings.py | 27 +++++++++++-------- backend/tests/e2e/test_logging_e2e.py | 2 +- .../test_fastapi_logging_integration.py | 4 +-- backend/tests/unit/test_logging_settings.py | 21 ++++++++++++--- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/backend/src/core/config/settings.py b/backend/src/core/config/settings.py index 58f57ae..92c65ee 100644 --- a/backend/src/core/config/settings.py +++ b/backend/src/core/config/settings.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import ClassVar, Literal from urllib.parse import quote -from pydantic import BaseModel, Field, computed_field, field_validator +from pydantic import BaseModel, Field, computed_field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -21,8 +21,8 @@ class RuntimeSettings(BaseModel): log_rotation_max_bytes: int = 10_000_000 log_dir: str = "logs" log_error_dir: str = "logs/errors" - log_file_name: str = "app.log" - log_error_file_name: str = "error.log" + log_file_name: str = "" + log_error_file_name: str = "" log_sensitive_fields: list[str] = Field( default_factory=lambda: [ "password", @@ -47,15 +47,20 @@ class RuntimeSettings(BaseModel): def lock_log_error_dir(cls, _: object) -> str: return "logs/errors" - @field_validator("log_file_name", mode="before") - @classmethod - def lock_log_file_name(cls, _: object) -> str: - return "app.log" + @model_validator(mode="after") + def ensure_service_scoped_log_file_names(self) -> "RuntimeSettings": + service = "".join( + char if char.isalnum() or char in {"-", "_"} else "-" + for char in self.service_name + ).strip("-_") + service_name = service or "app" - @field_validator("log_error_file_name", mode="before") - @classmethod - def lock_log_error_file_name(cls, _: object) -> str: - return "error.log" + if not self.log_file_name.strip(): + self.log_file_name = f"{service_name}.log" + if not self.log_error_file_name.strip(): + self.log_error_file_name = f"{service_name}.error.log" + + return self class CelerySettings(BaseModel): diff --git a/backend/tests/e2e/test_logging_e2e.py b/backend/tests/e2e/test_logging_e2e.py index 6fdbaa2..ff86fa6 100644 --- a/backend/tests/e2e/test_logging_e2e.py +++ b/backend/tests/e2e/test_logging_e2e.py @@ -86,7 +86,7 @@ def test_e2e_error_logging(tmp_path: Path) -> None: server.should_exit = True thread.join(timeout=5) - error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log") + error_entries = _read_json_lines(Path(tmp_path) / "errors" / "app.error.log") entry = next( item for item in error_entries if item.get("message") == "Unhandled exception" ) diff --git a/backend/tests/integration/test_fastapi_logging_integration.py b/backend/tests/integration/test_fastapi_logging_integration.py index 88e3581..3b521d4 100644 --- a/backend/tests/integration/test_fastapi_logging_integration.py +++ b/backend/tests/integration/test_fastapi_logging_integration.py @@ -83,7 +83,7 @@ def test_exception_handler_logs_stack_and_sends_500(tmp_path: Path) -> None: assert response.status_code == 500 assert response.json()["detail"] == "Internal Server Error" - error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log") + error_entries = _read_json_lines(Path(tmp_path) / "errors" / "app.error.log") assert error_entries entry = error_entries[-1] assert entry["level"] == "error" @@ -116,7 +116,7 @@ def test_invalid_request_id_is_replaced_and_used_in_error_context( response_request_id = response.headers["X-Request-ID"] assert response_request_id != "bad" - error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log") + error_entries = _read_json_lines(Path(tmp_path) / "errors" / "app.error.log") assert error_entries entry = error_entries[-1] assert entry["request_id"] == response_request_id diff --git a/backend/tests/unit/test_logging_settings.py b/backend/tests/unit/test_logging_settings.py index 62447de..df1c54e 100644 --- a/backend/tests/unit/test_logging_settings.py +++ b/backend/tests/unit/test_logging_settings.py @@ -17,7 +17,7 @@ def test_runtime_settings_defaults() -> None: assert settings.runtime.log_dir == "logs" assert settings.runtime.log_error_dir == "logs/errors" assert settings.runtime.log_file_name == "app.log" - assert settings.runtime.log_error_file_name == "error.log" + assert settings.runtime.log_error_file_name == "app.error.log" assert "password" in settings.runtime.log_sensitive_fields @@ -33,7 +33,22 @@ def test_runtime_settings_env_override(monkeypatch: MonkeyPatch) -> None: assert settings.runtime.log_dir == "logs" assert settings.runtime.log_error_dir == "logs/errors" - assert settings.runtime.log_file_name == "app.log" - assert settings.runtime.log_error_file_name == "error.log" + assert settings.runtime.log_file_name == "custom.log" + assert settings.runtime.log_error_file_name == "custom-error.log" assert settings.runtime.log_rotation == "size" assert settings.runtime.log_rotation_max_bytes == 2048 + + +def test_runtime_settings_default_file_names_follow_service_name( + monkeypatch: MonkeyPatch, +) -> None: + monkeypatch.delenv("SOCIAL_RUNTIME__LOG_FILE_NAME", raising=False) + monkeypatch.delenv("SOCIAL_RUNTIME__LOG_ERROR_FILE_NAME", raising=False) + monkeypatch.setenv("SOCIAL_RUNTIME__SERVICE_NAME", "worker-default") + + settings = Settings() + + assert settings.runtime.log_dir == "logs" + assert settings.runtime.log_error_dir == "logs/errors" + assert settings.runtime.log_file_name == "worker-default.log" + assert settings.runtime.log_error_file_name == "worker-default.error.log"