-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstructured_logging.py
More file actions
103 lines (89 loc) · 2.9 KB
/
Copy pathstructured_logging.py
File metadata and controls
103 lines (89 loc) · 2.9 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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import json
import logging
import time
from datetime import datetime, timezone
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
logger = logging.getLogger(__name__)
class JsonLogFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"timestamp": datetime.fromtimestamp(
record.created,
tz=timezone.utc,
).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
for field in (
"method",
"path",
"status_code",
"latency_ms",
"request_id",
"user_id",
"error_type",
):
if hasattr(record, field):
payload[field] = getattr(record, field)
return json.dumps(payload, default=str)
def configure_logging(log_format: str = "json") -> None:
formatter: logging.Formatter
if log_format == "json":
formatter = JsonLogFormatter()
else:
formatter = logging.Formatter(
"%(asctime)s %(levelname)s %(name)s %(message)s"
)
root_logger = logging.getLogger()
if not root_logger.handlers:
root_logger.addHandler(logging.StreamHandler())
for handler in root_logger.handlers:
handler.setFormatter(formatter)
class StructuredLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
started_at = time.perf_counter()
try:
response = await call_next(request)
except Exception as exc:
self._log_request(
request=request,
started_at=started_at,
status_code=500,
level=logging.ERROR,
message="request failed",
error_type=type(exc).__name__,
)
raise
self._log_request(
request=request,
started_at=started_at,
status_code=response.status_code,
level=logging.INFO,
message="request completed",
)
return response
@staticmethod
def _log_request(
request: Request,
started_at: float,
status_code: int,
level: int,
message: str,
error_type: str | None = None,
) -> None:
latency_ms = round((time.perf_counter() - started_at) * 1000, 2)
logger.log(
level,
message,
extra={
"method": request.method,
"path": request.url.path,
"status_code": status_code,
"latency_ms": latency_ms,
"request_id": getattr(request.state, "request_id", None),
"user_id": getattr(request.state, "user_id", None),
"error_type": error_type,
},
)