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.

133 lines
4.0 KiB
Python

import logging
import json
import datetime
import os
import sys
from typing import Optional
from src.config import LOG_LEVEL
from src.enums import RBDEnum
LOG_FORMAT_DEBUG = "%(levelname)s:%(message)s:%(pathname)s:%(funcName)s:%(lineno)d"
# 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
}
class LogLevels(RBDEnum):
info = "INFO"
warn = "WARN"
error = "ERROR"
debug = "DEBUG"
class JSONFormatter(logging.Formatter):
"""
Custom formatter to output logs in JSON format.
"""
def format(self, record):
from src.context import get_request_id
request_id = None
try:
request_id = get_request_id()
except Exception:
pass
log_record = {
"timestamp": datetime.datetime.fromtimestamp(record.created).astimezone().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"logger_name": record.name,
"location": f"{record.module}:{record.funcName}:{record.lineno}",
"module": record.module,
"funcName": record.funcName,
"lineno": record.lineno,
"pid": os.getpid(),
"request_id": request_id,
}
# Capture exception info if available
if record.exc_info:
log_record["exception"] = self.formatException(record.exc_info)
# Capture stack info if available
if record.stack_info:
log_record["stack_trace"] = self.formatStack(record.stack_info)
# Add any extra attributes passed to the log call
# We skip standard attributes to avoid duplication
standard_attrs = {
"args", "asctime", "created", "exc_info", "exc_text", "filename",
"funcName", "levelname", "levelno", "lineno", "module", "msecs",
"message", "msg", "name", "pathname", "process", "processName",
"relativeCreated", "stack_info", "thread", "threadName"
}
for key, value in record.__dict__.items():
if key not in standard_attrs:
log_record[key] = value
log_json = json.dumps(log_record)
# Apply color if the output is a terminal
if sys.stdout.isatty():
level_color = COLORS.get(record.levelname, "")
return f"{level_color}{log_json}{RESET}"
return log_json
def configure_logging():
log_level = str(LOG_LEVEL).upper() # cast to string
log_levels = list(LogLevels)
if log_level not in log_levels:
log_level = LogLevels.error
# Get the root logger
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# Clear existing handlers to avoid duplicate logs
if root_logger.hasHandlers():
root_logger.handlers.clear()
# Create a stream handler that outputs to stdout
handler = logging.StreamHandler(sys.stdout)
# Use JSONFormatter for all environments, or could be conditional
# For now, let's assume the user wants JSON everywhere as requested
formatter = JSONFormatter()
# If debug mode is specifically requested and we want the old format for debug:
# if log_level == LogLevels.debug:
# formatter = logging.Formatter(LOG_FORMAT_DEBUG)
handler.setFormatter(formatter)
root_logger.addHandler(handler)
for logger_name in ["uvicorn", "uvicorn.error", "fastapi"]:
logger = logging.getLogger(logger_name)
logger.handlers = []
logger.propagate = True
for logger_name in ["uvicorn.access"]:
logger = logging.getLogger(logger_name)
logger.handlers = []
logger.propagate = False
# # sometimes the slack client can be too verbose
# logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL)