Skip to content

Commit fc3a800

Browse files
committed
Enable robust logging for different environments
1 parent bcd8c37 commit fc3a800

File tree

2 files changed

+136
-13
lines changed

2 files changed

+136
-13
lines changed

src/app/core/logger.py

Lines changed: 132 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,140 @@
11
import logging
2+
import logging.config
23
import os
3-
from logging.handlers import RotatingFileHandler
4+
from datetime import UTC, datetime
5+
from pathlib import Path
6+
from typing import Any
47

5-
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs")
6-
if not os.path.exists(LOG_DIR):
7-
os.makedirs(LOG_DIR)
8+
from pythonjsonlogger.json import JsonFormatter
89

9-
LOG_FILE_PATH = os.path.join(LOG_DIR, "app.log")
10+
from .config import EnvironmentOption, settings
1011

11-
LOGGING_LEVEL = logging.INFO
12-
LOGGING_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
1312

14-
logging.basicConfig(level=LOGGING_LEVEL, format=LOGGING_FORMAT)
13+
class ColoredFormatter(logging.Formatter):
14+
"""Colored formatter for development console output."""
1515

16-
file_handler = RotatingFileHandler(LOG_FILE_PATH, maxBytes=10485760, backupCount=5)
17-
file_handler.setLevel(LOGGING_LEVEL)
18-
file_handler.setFormatter(logging.Formatter(LOGGING_FORMAT))
16+
COLORS = {
17+
"DEBUG": "\033[36m", # Cyan
18+
"INFO": "\033[32m", # Green
19+
"WARNING": "\033[33m", # Yellow
20+
"ERROR": "\033[31m", # Red
21+
"CRITICAL": "\033[35m", # Magenta
22+
}
23+
RESET = "\033[0m"
1924

20-
logging.getLogger("").addHandler(file_handler)
25+
def format(self, record: logging.LogRecord) -> str:
26+
# Create a copy of the record to avoid modifying the original
27+
record_copy = logging.makeLogRecord(record.__dict__)
28+
log_color = self.COLORS.get(record_copy.levelname, "")
29+
record_copy.levelname = f"{log_color}{record_copy.levelname}{self.RESET}"
30+
return super().format(record_copy)
31+
32+
33+
def get_log_level() -> int:
34+
"""Get log level from environment with validation."""
35+
log_level_name = os.getenv("LOG_LEVEL", "INFO").upper()
36+
37+
level = logging.getLevelNamesMapping().get(log_level_name)
38+
if level is None:
39+
raise ValueError(f"Invalid log level '{log_level_name}'")
40+
41+
return level
42+
43+
44+
def log_directory() -> Path:
45+
"""Ensure log directory exists and return the path."""
46+
log_dir = Path(__file__).parent.parent.parent / "logs"
47+
log_dir.mkdir(parents=True, exist_ok=True)
48+
return log_dir
49+
50+
51+
def get_logging_config() -> dict[str, Any]:
52+
"""Get logging configuration based on environment."""
53+
log_level = get_log_level()
54+
55+
# Base configuration
56+
config: dict[str, Any] = {
57+
"version": 1,
58+
"disable_existing_loggers": False,
59+
"formatters": {
60+
"development": {
61+
"()": ColoredFormatter,
62+
"format": "%(asctime)s- %(levelname)s - %(name)s - %(message)s",
63+
"datefmt": "%Y-%m-%d %H:%M:%S",
64+
},
65+
"file": {
66+
"format": "%(asctime)s- %(levelname)s - %(name)s - %(message)s",
67+
"datefmt": "%Y-%m-%d %H:%M:%S",
68+
},
69+
"json": {
70+
"()": JsonFormatter,
71+
"format": "%(asctime)s %(levelname)s %(name)s %(message)s %(pathname)s %(lineno)d",
72+
},
73+
},
74+
"handlers": {
75+
"console": {"class": "logging.StreamHandler", "level": log_level, "stream": "ext://sys.stdout"},
76+
},
77+
"root": {"level": log_level, "handlers": []},
78+
"loggers": {
79+
"uvicorn.access": {
80+
"level": "INFO",
81+
"handlers": [],
82+
"propagate": False, # Don't propagate to root logger to avoid double logging
83+
},
84+
"uvicorn.error": {"level": "INFO"},
85+
"sqlalchemy.engine": {"level": "WARNING"}, # Hide SQL queries unless warning/error
86+
"sqlalchemy.pool": {"level": "WARNING"},
87+
"httpx": {"level": "WARNING"}, # External HTTP client logs
88+
"httpcore": {"level": "WARNING"},
89+
},
90+
}
91+
92+
# Environment-specific configuration
93+
if settings.ENVIRONMENT == EnvironmentOption.LOCAL:
94+
# Create file handler only when needed
95+
log_dir = log_directory()
96+
# Keeping filename timestamp granularity to minutes to avoid too
97+
# many log files during development and reloding. Keeping it human
98+
# readable for easier debugging using 3 letter month, in AM/PM format
99+
# and without the year. It has to be in UTC as it runs in containers.
100+
timestamp = datetime.now(UTC).strftime("%d-%b_%I-%M%p_UTC")
101+
log_file = log_dir / f"web_{timestamp}.log"
102+
103+
config["handlers"]["file"] = {
104+
"class": "logging.handlers.RotatingFileHandler",
105+
"level": log_level,
106+
"filename": str(log_file),
107+
"maxBytes": 10485760, # 10MB
108+
"backupCount": 5,
109+
"formatter": "file",
110+
}
111+
112+
# Plain colored text + file logging
113+
config["handlers"]["console"]["formatter"] = "development"
114+
config["root"]["handlers"] = ["console", "file"]
115+
config["loggers"]["uvicorn.access"]["handlers"] = ["console", "file"]
116+
else:
117+
# As JSON messages to console
118+
config["handlers"]["console"]["formatter"] = "json"
119+
config["root"]["handlers"] = ["console"]
120+
config["loggers"]["uvicorn.access"]["handlers"] = ["console"]
121+
122+
return config
123+
124+
125+
def setup_logging() -> None:
126+
"""Setup logging configuration based on environment."""
127+
config = get_logging_config()
128+
logging.config.dictConfig(config)
129+
130+
# Log startup information
131+
logger = logging.getLogger(__name__)
132+
logger.info(f"Logging configured for {settings.ENVIRONMENT.value} environment")
133+
logger.info(f"Log level set to {logging.getLevelName(get_log_level())}")
134+
if "file" in config["root"]["handlers"]:
135+
logger.info(f"Logs will be written to the file {config['handlers']['file']['filename']}")
136+
if "console" in config["root"]["handlers"]:
137+
extra = ""
138+
if config["handlers"]["console"]["formatter"] == "json":
139+
extra = " in JSON format"
140+
logger.info(f"Logs will be written to the console{extra}")

src/app/core/setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from fastapi.openapi.utils import get_openapi
1414

1515
from ..api.dependencies import get_current_superuser
16+
from ..core.logger import setup_logging
1617
from ..core.utils.rate_limit import rate_limiter
1718
from ..middleware.client_cache_middleware import ClientCacheMiddleware
1819
from ..models import * # noqa: F403
@@ -188,7 +189,9 @@ def create_application(
188189
for caching, queue, and rate limiting, client-side caching, and customizing the API documentation
189190
based on the environment settings.
190191
"""
191-
# --- before creating application ---
192+
# Setup logging first based on the environment, before any other operations
193+
setup_logging()
194+
192195
if isinstance(settings, AppSettings):
193196
to_update = {
194197
"title": settings.APP_NAME,

0 commit comments

Comments
 (0)