127 lines
3.7 KiB
Python
127 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from core.config.settings import Settings
|
|
from core.logging.config import configure_logging
|
|
from core.logging.logger import get_logger
|
|
from core.logging.middleware import (
|
|
RequestContextMiddleware,
|
|
register_exception_handlers,
|
|
)
|
|
|
|
|
|
def _read_json_lines(path: Path) -> list[dict[str, object]]:
|
|
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
|
|
|
|
|
|
def _configure_test_logging(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,
|
|
}
|
|
)
|
|
test_settings = settings.model_copy(update={"runtime": runtime})
|
|
|
|
configure_logging(test_settings)
|
|
|
|
|
|
def test_middleware_binds_request_context(tmp_path: Path) -> None:
|
|
_configure_test_logging(tmp_path)
|
|
|
|
app = FastAPI()
|
|
app.add_middleware(RequestContextMiddleware) # type: ignore[arg-type]
|
|
|
|
@app.get("/ok")
|
|
async def ok() -> dict[str, str]:
|
|
logger = get_logger("tests.ok")
|
|
logger.info("request accepted", context_key="context_value")
|
|
return {"status": "ok"}
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/ok", headers={"X-Request-ID": "req-1234"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["X-Request-ID"] == "req-1234"
|
|
|
|
log_entries = _read_json_lines(Path(tmp_path) / "app.log")
|
|
entry = next(
|
|
item for item in log_entries if item.get("message") == "request accepted"
|
|
)
|
|
assert entry["message"] == "request accepted"
|
|
assert entry["request_id"] == "req-1234"
|
|
assert entry["method"] == "GET"
|
|
assert entry["path"] == "/ok"
|
|
assert entry["context_key"] == "context_value"
|
|
|
|
logging.shutdown()
|
|
|
|
|
|
def test_exception_handler_logs_stack_and_sends_500(tmp_path: Path) -> None:
|
|
_configure_test_logging(tmp_path)
|
|
|
|
app = FastAPI()
|
|
app.add_middleware(RequestContextMiddleware)
|
|
register_exception_handlers(app)
|
|
|
|
@app.get("/boom")
|
|
async def boom() -> dict[str, str]:
|
|
raise RuntimeError("boom")
|
|
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
response = client.get("/boom", headers={"X-Request-ID": "req-5000"})
|
|
|
|
assert response.status_code == 500
|
|
assert response.json()["detail"] == "Internal Server Error"
|
|
|
|
error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log")
|
|
assert error_entries
|
|
entry = error_entries[-1]
|
|
assert entry["level"] == "error"
|
|
assert entry["request_id"] == "req-5000"
|
|
exception = str(entry["exception"])
|
|
assert "Traceback" in exception
|
|
assert "test_fastapi_logging_integration" in exception
|
|
|
|
logging.shutdown()
|
|
|
|
|
|
def test_invalid_request_id_is_replaced_and_used_in_error_context(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_configure_test_logging(tmp_path)
|
|
|
|
app = FastAPI()
|
|
app.add_middleware(RequestContextMiddleware)
|
|
register_exception_handlers(app)
|
|
|
|
@app.get("/boom")
|
|
async def boom() -> dict[str, str]:
|
|
raise RuntimeError("boom")
|
|
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
response = client.get("/boom", headers={"X-Request-ID": "bad"})
|
|
|
|
assert response.status_code == 500
|
|
|
|
response_request_id = response.headers["X-Request-ID"]
|
|
assert response_request_id != "bad"
|
|
|
|
error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log")
|
|
assert error_entries
|
|
entry = error_entries[-1]
|
|
assert entry["request_id"] == response_request_id
|
|
exception = str(entry["exception"])
|
|
assert "Traceback" in exception
|
|
|
|
logging.shutdown()
|