Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/core/logging.py
2026-05-18 16:50:15 +00:00

214 lines
7.3 KiB
Python
Executable file

"""
پیکربندی متمرکز لاگ برای Hesabix API.
پیش‌فرض پروداکشن: حداقل خروجی (WARNING)، بدون فایل روی دیسک مگر با LOG_FILE.
برای عیب‌یابی: متغیر محیطی LOG_LEVEL=DEBUG (یا INFO) و سپس ری‌استارت سرویس.
توجه: در حالت چند worker، تغییر سطح فقط از طریق env + ری‌استارت معنادار است
(هر پروسس جداگانه configure می‌شود).
"""
from __future__ import annotations
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from typing import Any, Final
import structlog
# نام‌های استاندارد برای پاسخ API و مستندات
KNOWN_LEVELS: Final[tuple[str, ...]] = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
# آخرین پیکربندی اعمال‌شده روی این پروسس (برای diagnostics)
_LOG_META: dict[str, Any] = {}
def resolve_log_level(name: str | None) -> int:
"""تبدیل نام سطح به عدد logging؛ نام نامعتبر → WARNING."""
if not name:
return logging.WARNING
key = str(name).strip().upper()
mapping = logging.getLevelNamesMapping()
return int(mapping.get(key, logging.WARNING))
def logging_level_name(level: int) -> str:
"""برچسب خوانا برای یک سطح عددی."""
name = logging.getLevelName(level)
return str(name)
def get_logging_diagnostics() -> dict[str, Any]:
"""
وضعیت فعلی لاگ‌گذاری (برای پنل ادمین / عیب‌یابی).
ری‌استارت برای اعمال LOG_LEVEL جدید لازم است.
"""
root = logging.getLogger()
log_file = os.getenv("LOG_FILE", "").strip()
def _level(name: str) -> dict[str, Any]:
lg = logging.getLogger(name)
effective = lg.getEffectiveLevel()
return {
"name": name,
"level": logging_level_name(lg.level) if lg.level > 0 else "NOTSET",
"effective_level": logging_level_name(effective),
"propagate": lg.propagate,
}
return {
"configured_log_level": _LOG_META.get("configured_log_level"),
"effective_root_level": _LOG_META.get("effective_root_level") or logging_level_name(root.level),
"root": {
"level": logging_level_name(root.level),
"handlers": [type(h).__name__ for h in root.handlers],
},
"log_file_env": log_file or None,
"log_format": os.getenv("LOG_FORMAT", "json").strip().lower() or "json",
"known_levels": list(KNOWN_LEVELS),
"hint": "برای دیباگ معمولاً LOG_LEVEL=DEBUG در محیط سرویس و سپس systemctl restart hesabix-api",
"loggers": [
_level("uvicorn"),
_level("uvicorn.access"),
_level("uvicorn.error"),
_level("fastapi"),
_level("sqlalchemy.engine"),
_level("sqlalchemy.pool"),
_level("httpx"),
_level("httpcore"),
],
}
def _stdlib_processors() -> list[Any]:
"""پردازنده‌های مشترک structlog قبل از رندر نهایی."""
return [
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
]
def _build_structlog_chain(log_format: str) -> tuple[list[Any], Any]:
fmt = (log_format or "json").strip().lower()
if fmt in ("text", "plain", "console", "human"):
return (
[
*_stdlib_processors(),
structlog.dev.ConsoleRenderer(colors=os.getenv("LOG_COLORS", "").strip().lower() in {"1", "true", "yes"}),
],
structlog.PrintLoggerFactory(),
)
# پیش‌فرض: JSON برای journald / جمع‌آورنده‌های متمرکز
return (
[
*_stdlib_processors(),
structlog.processors.JSONRenderer(),
],
structlog.PrintLoggerFactory(file=sys.stdout),
)
def _apply_third_party_levels(app_level: int) -> None:
"""کاهش سروصدا در حالت غیر-DEBUG."""
# uvicorn: دسترسی HTTP معمولاً INFO است — در WARNING و بالاتر خاموش می‌شود
if app_level <= logging.DEBUG:
access = logging.DEBUG
elif app_level <= logging.INFO:
access = logging.INFO
else:
access = logging.WARNING
logging.getLogger("uvicorn").setLevel(app_level)
logging.getLogger("uvicorn.error").setLevel(app_level)
logging.getLogger("uvicorn.access").setLevel(access)
logging.getLogger("fastapi").setLevel(logging.WARNING if app_level >= logging.WARNING else app_level)
if app_level <= logging.DEBUG:
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
else:
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING if app_level >= logging.WARNING else app_level)
logging.getLogger("httpcore").setLevel(logging.WARNING if app_level >= logging.WARNING else app_level)
def configure_logging(settings: Any) -> None:
"""
اعمال پیکربندی لاگ روی همین پروسس.
settings.log_level: رشته‌ای مثل WARNING / DEBUG (از pydantic-settings / .env معمولاً LOG_LEVEL).
"""
log_level_name = getattr(settings, "log_level", None) or "WARNING"
app_level = resolve_log_level(str(log_level_name))
log_format = os.getenv("LOG_FORMAT", "json").strip().lower() or "json"
processors, factory = _build_structlog_chain(log_format)
structlog.configure(
processors=processors,
wrapper_class=structlog.make_filtering_bound_logger(app_level),
logger_factory=factory,
cache_logger_on_first_use=True,
)
root_logger = logging.getLogger()
root_logger.setLevel(app_level)
root_logger.handlers.clear()
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(app_level)
# خروجی استاندارد کتابخانه‌ها (غیر structlog) — فشرده برای پروداکشن
console_handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(levelname)s [%(name)s] %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
)
root_logger.addHandler(console_handler)
# فقط اگر صریحاً مسیر فایل داده شده باشد (پیش‌فرض: بدون فایل، فقط stdout/journal)
log_file = os.getenv("LOG_FILE", "").strip()
if log_file and log_file.lower() not in {"stdout", "-", "none", "off"}:
try:
log_dir = os.path.dirname(log_file)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
file_handler = RotatingFileHandler(
log_file,
maxBytes=int(os.getenv("LOG_FILE_MAX_BYTES", str(50 * 1024 * 1024))),
backupCount=int(os.getenv("LOG_FILE_BACKUP_COUNT", "5")),
encoding="utf-8",
)
file_handler.setLevel(app_level)
file_handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(levelname)s [%(name)s] %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
)
root_logger.addHandler(file_handler)
except OSError as e:
logging.getLogger(__name__).warning("Could not start file logging (%s): %s", log_file, e)
_apply_third_party_levels(app_level)
# uvicorn گاهی handler جدا روی child logger می‌گذارد؛ برای یکپارچگی با root پاک می‌کنیم
for _name in ("uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"):
_lg = logging.getLogger(_name)
_lg.handlers.clear()
_lg.propagate = True
_LOG_META.clear()
_LOG_META.update(
{
"configured_log_level": str(log_level_name).strip().upper(),
"log_format": log_format,
"effective_root_level": logging_level_name(app_level),
}
)