Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
83 lines (65 loc) · 3.01 KB
/
Copy pathapp.py
File metadata and controls
83 lines (65 loc) · 3.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""The whole integration. Everything else here is ordinary code that happens to log."""
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
import gfmodules.logging as gflog
from fastapi_app import service
from fastapi_app.config import Settings
from fastapi_app.events import Log
from gfmodules.logging.exceptions import log_unhandled_exception
from gfmodules.logging.middleware import RequestContextMiddleware, restore_request_context
logger = logging.getLogger("app.api")
class CreateResource(BaseModel):
resource_id: str
owner_id: str
created_by: str
def setup_logging(settings: Settings, *, strict_fields: bool = False) -> None:
"""The test suite passes ``strict_fields=True`` so a field that would reach
no stream fails there rather than going quietly missing in production.
"""
gflog.configure(
config=settings.logging,
loglevel=settings.app.loglevel,
catalogue=Log,
strict_fields=strict_fields,
)
gflog.install_excepthook(logger)
gflog.install_signal_handlers()
def create_app(settings: Settings | None = None) -> FastAPI:
settings = settings if settings is not None else Settings()
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
async with gflog.lifespan_logging(logger, version=settings.app.version, config_path="/etc/app/config.toml"):
yield
app = FastAPI(lifespan=lifespan)
@app.post("/resources")
async def create(body: CreateResource) -> Response:
service.create_resource(body.resource_id, body.owner_id, body.created_by)
return JSONResponse(status_code=201, content={"resource_id": body.resource_id})
@app.delete("/resources/{resource_id}")
async def delete(resource_id: str) -> Response:
try:
service.delete_resource(resource_id, reason="requested by owner")
except service.UnknownResource:
return JSONResponse(status_code=404, content={"error": "Unknown resource"})
return JSONResponse({"resource_id": resource_id})
@app.get("/boom")
async def boom() -> Response:
raise RuntimeError("something the application did not anticipate")
@restore_request_context
async def unhandled(request: Request, exc: Exception) -> Response:
log_unhandled_exception(logger, request, exc)
return JSONResponse(status_code=500, content={"error": "Internal server error"})
app.add_exception_handler(Exception, unhandled)
# Added last so it runs outermost, wrapping every route and handler above.
app.add_middleware(
RequestContextMiddleware,
correlation_id_expected=settings.logging.correlation_id_expected,
# capture_body_methods stays unset: no reason to log a create call's contents.
trust_forwarded_for=settings.logging.trust_forwarded_for,
)
return app