|
1 | 1 | import logging |
2 | | -import logging.config |
3 | | - |
4 | | - |
5 | | -LOGGING_CONFIG = { |
6 | | - "version": 1, |
7 | | - "disable_existing_loggers": False, # keeps Uvicorn's loggers |
8 | | - "formatters": { |
9 | | - "default": { |
10 | | - "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s", |
11 | | - }, |
12 | | - }, |
13 | | - "handlers": { |
14 | | - "console": { |
15 | | - "class": "logging.StreamHandler", |
16 | | - "formatter": "default", |
17 | | - }, |
18 | | - }, |
19 | | - "root": { # applies to all loggers unless overridden |
20 | | - "level": "INFO", |
21 | | - "handlers": ["console"], |
22 | | - }, |
23 | | - "loggers": { |
24 | | - "uvicorn": {"level": "INFO"}, |
25 | | - "uvicorn.error": {"level": "INFO"}, |
26 | | - "uvicorn.access": {"level": "INFO"}, |
27 | | - # custom API loggers |
28 | | - "app.routers": {"level": "DEBUG"}, # all your routers |
29 | | - "app.services": {"level": "DEBUG"}, # all your services |
30 | | - "app.platforms": {"level": "DEBUG"}, # all platform implementations |
31 | | - }, |
32 | | -} |
| 2 | +import os |
| 3 | +import sys |
| 4 | + |
| 5 | + |
| 6 | +from app.middleware.correlation_id import correlation_id_ctx |
| 7 | +from loguru import logger |
| 8 | + |
| 9 | + |
| 10 | +class InterceptHandler(logging.Handler): |
| 11 | + """ |
| 12 | + Redirect standard logging (incl. uvicorn) to Loguru. |
| 13 | + """ |
| 14 | + |
| 15 | + def emit(self, record): |
| 16 | + try: |
| 17 | + level = logger.level(record.levelname).name |
| 18 | + corr_id = correlation_id_ctx.get() |
| 19 | + except ValueError: |
| 20 | + level = record.levelno |
| 21 | + except LookupError: |
| 22 | + corr_id = None |
| 23 | + |
| 24 | + frame, depth = logging.currentframe(), 2 |
| 25 | + while frame and frame.f_code.co_filename == logging.__file__: |
| 26 | + frame = frame.f_back |
| 27 | + depth += 1 |
| 28 | + logger.opt(depth=depth, exception=record.exc_info).log( |
| 29 | + level, record.getMessage() |
| 30 | + ) |
| 31 | + if corr_id: |
| 32 | + logger.bind(correlation_id=corr_id) |
| 33 | + |
| 34 | + |
| 35 | +def correlation_id_filter(record): |
| 36 | + # Always inject the current correlation ID into the log record |
| 37 | + record["extra"]["correlation_id"] = correlation_id_ctx.get() |
| 38 | + return True |
33 | 39 |
|
34 | 40 |
|
35 | 41 | def setup_logging(): |
36 | | - logging.config.dictConfig(LOGGING_CONFIG) |
| 42 | + logger.remove() # remove default handler |
| 43 | + env = os.getenv("APP_ENV", "development") |
| 44 | + |
| 45 | + logger.configure(extra={"correlation_id": None}) |
| 46 | + if env == "production": |
| 47 | + # JSON logs for ELK |
| 48 | + logger.add( |
| 49 | + sys.stdout, |
| 50 | + serialize=True, |
| 51 | + backtrace=False, |
| 52 | + diagnose=False, |
| 53 | + level="INFO", |
| 54 | + filter=correlation_id_filter, |
| 55 | + ) |
| 56 | + else: |
| 57 | + # Pretty logs for dev |
| 58 | + logger.add( |
| 59 | + sys.stdout, |
| 60 | + colorize=True, |
| 61 | + format=( |
| 62 | + "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | " |
| 63 | + "<i>{extra[correlation_id]}</i> | " |
| 64 | + "<level>{level: <8}</level> | " |
| 65 | + "<cyan>{name}</cyan>:<cyan>{function}</cyan> :<cyan>{line}</cyan> - " |
| 66 | + "{message}" |
| 67 | + ), |
| 68 | + backtrace=True, |
| 69 | + diagnose=True, |
| 70 | + level="DEBUG", |
| 71 | + filter=correlation_id_filter, |
| 72 | + ) |
| 73 | + |
| 74 | + for name in ( |
| 75 | + "uvicorn", |
| 76 | + "uvicorn.error", |
| 77 | + "uvicorn.access", |
| 78 | + "fastapi", |
| 79 | + "app.routers", |
| 80 | + "app.services", |
| 81 | + "app.platforms", |
| 82 | + ): |
| 83 | + logging.getLogger(name).handlers = [InterceptHandler()] |
| 84 | + logging.getLogger(name).propagate = False |
| 85 | + |
| 86 | + return logger |
0 commit comments