You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
be-optimumoh/src/app_logging.py

132 lines
4.2 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import logging
import logging.handlers
import json
import datetime
import os
import sys
try:
from zoneinfo import ZoneInfo
except ImportError:
from backports.zoneinfo import ZoneInfo # type: ignore[no-redef]
from src.config import LOG_LEVEL, SERVICE_NAME
from src.enums import OptimumOHEnum
_TZ_LOCAL = ZoneInfo("Asia/Jakarta")
# ANSI Color Codes
RESET = "\033[0m"
COLORS = {
"DEBUG": "\033[36m", # Cyan
"INFO": "\033[32m", # Green
"WARNING": "\033[33m", # Yellow
"WARN": "\033[33m", # Yellow
"ERROR": "\033[31m", # Red
"CRITICAL": "\033[1;31m", # Bold Red
}
# Fields in the log record injected by the logging middleware or extra= kwargs
_REQUEST_FIELDS = frozenset({
"method", "path", "status_code", "duration_ms",
"request_id", "user_id", "user_ip",
"error_id", "result", "error_type", "error_message",
})
class LogLevels(OptimumOHEnum):
info = "INFO"
warn = "WARN"
error = "ERROR"
debug = "DEBUG"
class JSONFormatter(logging.Formatter):
"""
Outputs exactly 14 fixed fields per log line in JSON format.
Fields (always present, null when not applicable):
timestamp, service_name, level, method, path, status_code,
duration_ms, request_id, user_id, user_ip, error_id,
result, error_type, error_message
"""
def format(self, record: logging.LogRecord) -> str:
dt = datetime.datetime.fromtimestamp(record.created, tz=_TZ_LOCAL)
timestamp = dt.isoformat(timespec="microseconds")
status_code = getattr(record, "status_code", None)
result = getattr(record, "result", None)
if result is None and status_code is not None:
result = "Request completed" if int(status_code) < 400 else "Request failed"
log_record = {
"timestamp": timestamp,
"service_name": SERVICE_NAME,
"level": record.levelname,
"method": getattr(record, "method", None),
"path": getattr(record, "path", None),
"status_code": status_code,
"duration_ms": getattr(record, "duration_ms", None),
"request_id": getattr(record, "request_id", None),
"user_id": getattr(record, "user_id", None),
"user_ip": getattr(record, "user_ip", None),
"error_id": getattr(record, "error_id", None),
"result": result,
"error_type": getattr(record, "error_type", None),
"error_message": getattr(record, "error_message", None),
}
log_json = json.dumps(log_record, default=str)
if sys.stdout.isatty():
color = COLORS.get(record.levelname, "")
return f"{color}{log_json}{RESET}"
return log_json
def configure_logging() -> None:
log_level = str(LOG_LEVEL).upper()
if log_level not in list(LogLevels):
log_level = LogLevels.error
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
if root_logger.hasHandlers():
root_logger.handlers.clear()
formatter = JSONFormatter()
# stdout handler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(formatter)
root_logger.addHandler(stream_handler)
# rotating file handler — logs/app.log 10 MB × 5 backups
os.makedirs("logs", exist_ok=True)
file_handler = logging.handlers.RotatingFileHandler(
"logs/app.log",
maxBytes=10 * 1024 * 1024,
backupCount=5,
encoding="utf-8",
)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Reconfigure uvicorn loggers to propagate through our formatter
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]:
uvicorn_logger = logging.getLogger(logger_name)
uvicorn_logger.handlers = []
uvicorn_logger.propagate = True
# Silence chatty loggers — we emit one structured log per request ourselves
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
slowapi_logger = logging.getLogger("slowapi")
slowapi_logger.setLevel(logging.ERROR)
slowapi_logger.propagate = False
logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL)