refactor: align repo layout and logging safeguards

This commit is contained in:
qzl
2026-01-29 17:02:09 +08:00
parent 6af0989fe7
commit c2e65fa157
56 changed files with 1881 additions and 890 deletions
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
from celery import Celery
from pytest import MonkeyPatch
from core.logging import celery as celery_logging
from core.logging.context import clear_context, get_context
class DummyTask:
name: str = "tasks.sample"
def test_celery_prerun_binds_task_context() -> None:
handlers = celery_logging.build_celery_signal_handlers()
handlers.on_task_prerun(task_id="task-123", task=DummyTask())
context = get_context()
assert context["task_id"] == "task-123"
assert context["task_name"] == "tasks.sample"
clear_context()
def test_celery_setup_logging_calls_configure(monkeypatch: MonkeyPatch) -> None:
called = {"value": False}
def fake_configure_logging(settings: object | None = None) -> None:
called["value"] = True
monkeypatch.setattr(celery_logging, "configure_logging", fake_configure_logging)
handlers = celery_logging.build_celery_signal_handlers()
handlers.on_setup_logging()
assert called["value"] is True
def test_configure_celery_app_disables_hijack() -> None:
app = Celery("test")
celery_logging.configure_celery_app(app)
assert app.conf.worker_hijack_root_logger is False
+140
View File
@@ -0,0 +1,140 @@
from __future__ import annotations
import json
import logging
from collections.abc import Iterator
from pathlib import Path
from typing import cast
import pytest
import structlog
from core.config.settings import Settings
from core.logging.config import build_logging_config, configure_logging
def _get_handlers(config: dict[str, object]) -> dict[str, dict[str, object]]:
return cast(dict[str, dict[str, object]], config["handlers"])
def test_build_logging_config_time_rotation(tmp_path: Path) -> None:
settings = Settings()
runtime = settings.runtime.model_copy(
update={
"log_dir": str(tmp_path),
"log_error_dir": str(tmp_path / "errors"),
"log_rotation": "time",
}
)
config = build_logging_config(runtime)
handlers = _get_handlers(config)
assert handlers["file"]["class"] == "logging.handlers.TimedRotatingFileHandler"
assert handlers["error"]["class"] == "logging.handlers.TimedRotatingFileHandler"
assert handlers["error"]["level"] == "ERROR"
def test_build_logging_config_size_rotation(tmp_path: Path) -> None:
settings = Settings()
runtime = settings.runtime.model_copy(
update={
"log_dir": str(tmp_path),
"log_error_dir": str(tmp_path / "errors"),
"log_rotation": "size",
"log_rotation_max_bytes": 2048,
}
)
config = build_logging_config(runtime)
handlers = _get_handlers(config)
assert handlers["file"]["class"] == "logging.handlers.RotatingFileHandler"
assert handlers["error"]["class"] == "logging.handlers.RotatingFileHandler"
assert handlers["file"]["maxBytes"] == 2048
def test_build_logging_config_plain_formatter_when_disabled(tmp_path: Path) -> None:
settings = Settings()
runtime = settings.runtime.model_copy(
update={
"log_dir": str(tmp_path),
"log_error_dir": str(tmp_path / "errors"),
"log_json": False,
}
)
config = build_logging_config(runtime)
handlers = _get_handlers(config)
assert handlers["file"]["formatter"] == "plain"
assert handlers["error"]["formatter"] == "plain"
def _read_last_log_entry(log_path: Path) -> dict[str, object]:
assert log_path.exists(), f"Expected log file at {log_path}"
entries = [
json.loads(line) for line in log_path.read_text().splitlines() if line.strip()
]
assert entries, "Expected at least one log entry in app.log"
return entries[-1]
def _flush_root_handlers() -> None:
root_logger = logging.getLogger()
for handler in root_logger.handlers:
if hasattr(handler, "flush"):
handler.flush()
@pytest.fixture
def configured_logging(tmp_path: Path) -> Iterator[Path]:
settings = Settings()
runtime = settings.runtime.model_copy(
update={
"log_dir": str(tmp_path),
"log_error_dir": str(tmp_path / "errors"),
"log_rotation": "size",
"log_rotation_max_bytes": 2048,
"log_json": True,
}
)
root_logger = logging.getLogger()
original_handlers = root_logger.handlers[:]
original_level = root_logger.level
configure_logging(settings.model_copy(update={"runtime": runtime}))
yield tmp_path
for handler in root_logger.handlers:
handler.close()
root_logger.handlers = original_handlers
root_logger.setLevel(original_level)
structlog.reset_defaults()
def test_stdlib_logging_redacts_sensitive_fields(configured_logging: Path) -> None:
logger = logging.getLogger("tests.stdlib")
logger.info("login", extra={"password": "secret", "token": "abc"})
_flush_root_handlers()
log_path = configured_logging / "app.log"
entry = _read_last_log_entry(log_path)
assert entry["password"] == "[REDACTED]"
assert entry["token"] == "[REDACTED]"
def test_structlog_redacts_sensitive_fields(configured_logging: Path) -> None:
logger = structlog.get_logger("tests.structlog")
logger.info("login", password="secret", token="abc")
_flush_root_handlers()
log_path = configured_logging / "app.log"
entry = _read_last_log_entry(log_path)
assert entry["password"] == "[REDACTED]"
assert entry["token"] == "[REDACTED]"
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from core.logging.filters import build_sensitive_data_processor
def test_redact_sensitive_fields_masks_values() -> None:
processor = build_sensitive_data_processor(
["password", "token", "api_key", "cookie"]
)
event: dict[str, object] = {
"message": "login",
"password": "secret",
"access_token": "token-123",
"apiKey": "apikey-123",
"set-cookie": "cookie-1",
"nested": {"token": "abc", "safe": "ok"},
"list": [{"password": "x"}],
}
redacted = processor(None, "info", event)
assert redacted["password"] == "[REDACTED]"
assert redacted["access_token"] == "[REDACTED]"
assert redacted["apiKey"] == "[REDACTED]"
assert redacted["set-cookie"] == "[REDACTED]"
assert redacted["nested"]["token"] == "[REDACTED]"
assert redacted["nested"]["safe"] == "ok"
assert redacted["list"][0]["password"] == "[REDACTED]"
assert event["password"] == "secret"
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from pytest import MonkeyPatch
from core.config.settings import Settings
def test_runtime_settings_defaults() -> None:
settings = Settings()
assert settings.runtime.log_json is True
assert settings.runtime.log_rotation == "time"
assert settings.runtime.log_rotation_when == "midnight"
assert settings.runtime.log_rotation_interval == 1
assert settings.runtime.log_rotation_backup_count == 14
assert settings.runtime.log_rotation_max_bytes == 10_000_000
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 "password" in settings.runtime.log_sensitive_fields
def test_runtime_settings_env_override(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setenv("SOCIAL_RUNTIME__LOG_DIR", "var/logs")
monkeypatch.setenv("SOCIAL_RUNTIME__LOG_ERROR_DIR", "var/logs/errors")
monkeypatch.setenv("SOCIAL_RUNTIME__LOG_ROTATION", "size")
monkeypatch.setenv("SOCIAL_RUNTIME__LOG_ROTATION_MAX_BYTES", "2048")
settings = Settings()
assert settings.runtime.log_dir == "var/logs"
assert settings.runtime.log_error_dir == "var/logs/errors"
assert settings.runtime.log_rotation == "size"
assert settings.runtime.log_rotation_max_bytes == 2048