Add Dispatch_V0.1.1
This commit is contained in:
268
Dispatch_V0.1.1/error_logger.py
Normal file
268
Dispatch_V0.1.1/error_logger.py
Normal file
@@ -0,0 +1,268 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# error_logger.py
|
||||
"""Централизованный модуль логирования ошибок.
|
||||
|
||||
Записывает ошибки в logs/error.log с ротацией и идентификацией источника.
|
||||
|
||||
Использование
|
||||
-------------
|
||||
::
|
||||
|
||||
from error_logger import setup_error_logging, log_exception
|
||||
|
||||
# Один раз при старте приложения:
|
||||
setup_error_logging()
|
||||
|
||||
# В except-блоках:
|
||||
except Exception as e:
|
||||
log_exception(__name__, "func_name", e)
|
||||
|
||||
Формат записи лога
|
||||
------------------
|
||||
::
|
||||
|
||||
2026-03-27 14:30:00 | ERROR | module | function | ExcType | message
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import faulthandler
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_LOG_DIR = Path(__file__).resolve().parent / "logs"
|
||||
_LOG_FILE = _LOG_DIR / "error.log"
|
||||
_INTERPRETER_LOG_FILE = _LOG_DIR / "interpreter.log"
|
||||
_MAX_BYTES = 5 * 1024 * 1024
|
||||
_BACKUP_COUNT = 3
|
||||
|
||||
_error_logger: logging.Logger = logging.getLogger("usms.errors")
|
||||
_configured = False
|
||||
_interpreter_hooks_installed = False
|
||||
_interpreter_stream = None
|
||||
|
||||
|
||||
class _SafeRotatingFileHandler(RotatingFileHandler):
|
||||
"""Rotating handler, устойчивый к lock-ошибкам Windows при rollover."""
|
||||
|
||||
def handleError(self, record: logging.LogRecord) -> None:
|
||||
exc = sys.exc_info()[1]
|
||||
if not _is_locked_rollover_error(exc):
|
||||
super().handleError(record)
|
||||
return
|
||||
self._reopen_stream()
|
||||
try:
|
||||
logging.FileHandler.emit(self, record)
|
||||
except Exception as exc:
|
||||
try:
|
||||
sys.__stderr__.write(f"error_logger emit failure: {type(exc).__name__}: {exc}\n")
|
||||
sys.__stderr__.flush()
|
||||
except Exception as _fallback_exc:
|
||||
return
|
||||
return
|
||||
|
||||
def _reopen_stream(self) -> None:
|
||||
try:
|
||||
if self.stream is not None:
|
||||
self.stream.close()
|
||||
except Exception as exc:
|
||||
try:
|
||||
sys.__stderr__.write(f"error_logger stream close failure: {type(exc).__name__}: {exc}\n")
|
||||
sys.__stderr__.flush()
|
||||
except Exception as _fallback_exc:
|
||||
return
|
||||
self.stream = self._open()
|
||||
|
||||
|
||||
def _is_locked_rollover_error(exc: BaseException | None) -> bool:
|
||||
"""Определить отказ rollover из-за блокировки log-файла на Windows."""
|
||||
if not isinstance(exc, PermissionError):
|
||||
return False
|
||||
return getattr(exc, "winerror", None) == 32
|
||||
|
||||
|
||||
def setup_error_logging() -> None:
|
||||
"""Настроить файловый handler для логирования ошибок.
|
||||
|
||||
Создаёт директорию ``logs/`` и добавляет ``RotatingFileHandler``
|
||||
к логгеру ``usms``, чтобы все дочерние логгеры
|
||||
(``usms.warehouse``, ``usms.errors`` и т.д.) автоматически
|
||||
записывали ошибки уровня ERROR и выше в файл.
|
||||
"""
|
||||
global _configured
|
||||
if _configured:
|
||||
return
|
||||
|
||||
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
usms_root = logging.getLogger("usms")
|
||||
|
||||
if any(
|
||||
isinstance(handler, _SafeRotatingFileHandler)
|
||||
and Path(getattr(handler, "baseFilename", "")) == _LOG_FILE
|
||||
for handler in usms_root.handlers
|
||||
):
|
||||
_configured = True
|
||||
return
|
||||
|
||||
file_handler = _SafeRotatingFileHandler(
|
||||
str(_LOG_FILE),
|
||||
maxBytes=_MAX_BYTES,
|
||||
backupCount=_BACKUP_COUNT,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setLevel(logging.ERROR)
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s | %(levelname)s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
usms_root.addHandler(file_handler)
|
||||
console_handler = logging.StreamHandler(sys.stderr)
|
||||
console_handler.setLevel(logging.ERROR)
|
||||
console_handler.setFormatter(formatter)
|
||||
usms_root.addHandler(console_handler)
|
||||
if usms_root.level == logging.NOTSET or usms_root.level > logging.DEBUG:
|
||||
usms_root.setLevel(logging.DEBUG)
|
||||
|
||||
_configured = True
|
||||
|
||||
|
||||
def install_interpreter_hooks() -> None:
|
||||
"""Подключить глобальные hooks для traceback и фатальных ошибок."""
|
||||
global _interpreter_hooks_installed, _interpreter_stream
|
||||
if _interpreter_hooks_installed:
|
||||
return
|
||||
|
||||
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_interpreter_stream = _INTERPRETER_LOG_FILE.open("a", encoding="utf-8")
|
||||
|
||||
def _handle_unhandled_exception(
|
||||
exc_type: type[BaseException],
|
||||
exc_value: BaseException,
|
||||
exc_tb,
|
||||
) -> None:
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
||||
return
|
||||
formatted = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
||||
_write_interpreter_diagnostics("UNHANDLED EXCEPTION", formatted)
|
||||
_error_logger.error(
|
||||
"%s | %s | %s\n%s",
|
||||
"__main__",
|
||||
"sys.excepthook",
|
||||
exc_type.__name__,
|
||||
formatted.rstrip(),
|
||||
)
|
||||
|
||||
def _handle_thread_exception(args: threading.ExceptHookArgs) -> None:
|
||||
formatted = "".join(
|
||||
traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)
|
||||
)
|
||||
_write_interpreter_diagnostics(
|
||||
f"THREAD EXCEPTION: {getattr(args.thread, 'name', 'unknown')}",
|
||||
formatted,
|
||||
)
|
||||
_error_logger.error(
|
||||
"%s | %s | %s\n%s",
|
||||
"__main__",
|
||||
"threading.excepthook",
|
||||
args.exc_type.__name__,
|
||||
formatted.rstrip(),
|
||||
)
|
||||
|
||||
def _handle_unraisable(unraisable) -> None:
|
||||
formatted = "".join(
|
||||
traceback.format_exception(
|
||||
type(unraisable.exc_value),
|
||||
unraisable.exc_value,
|
||||
unraisable.exc_traceback,
|
||||
)
|
||||
)
|
||||
object_repr = repr(getattr(unraisable, "object", None))
|
||||
_write_interpreter_diagnostics(
|
||||
f"UNRAISABLE EXCEPTION: {object_repr}",
|
||||
formatted,
|
||||
)
|
||||
_error_logger.error(
|
||||
"%s | %s | %s\n%s",
|
||||
"__main__",
|
||||
"sys.unraisablehook",
|
||||
type(unraisable.exc_value).__name__,
|
||||
formatted.rstrip(),
|
||||
)
|
||||
|
||||
sys.excepthook = _handle_unhandled_exception
|
||||
threading.excepthook = _handle_thread_exception
|
||||
if hasattr(sys, "unraisablehook"):
|
||||
sys.unraisablehook = _handle_unraisable
|
||||
|
||||
try:
|
||||
faulthandler.enable(file=_interpreter_stream, all_threads=True)
|
||||
except Exception as exc:
|
||||
try:
|
||||
sys.__stderr__.write(f"faulthandler enable failure: {type(exc).__name__}: {exc}\n")
|
||||
sys.__stderr__.flush()
|
||||
except Exception as _fallback_exc:
|
||||
return
|
||||
|
||||
_interpreter_hooks_installed = True
|
||||
|
||||
|
||||
def _write_interpreter_diagnostics(title: str, payload: str) -> None:
|
||||
"""Вывести интерпретаторную диагностику и в консоль, и в файл."""
|
||||
message = f"\n=== {title} ===\n{payload}"
|
||||
try:
|
||||
sys.__stderr__.write(message)
|
||||
sys.__stderr__.flush()
|
||||
except Exception as exc:
|
||||
try:
|
||||
sys.stderr.write(f"interpreter diagnostics stderr failure: {type(exc).__name__}: {exc}\n")
|
||||
sys.stderr.flush()
|
||||
except Exception as _fallback_exc:
|
||||
return
|
||||
try:
|
||||
if _interpreter_stream is not None:
|
||||
_interpreter_stream.write(message)
|
||||
_interpreter_stream.flush()
|
||||
except Exception as exc:
|
||||
try:
|
||||
sys.__stderr__.write(f"interpreter log file write failure: {type(exc).__name__}: {exc}\n")
|
||||
sys.__stderr__.flush()
|
||||
except Exception as _fallback_exc:
|
||||
return
|
||||
|
||||
|
||||
def log_exception(module: str, func: str, exc: BaseException) -> None:
|
||||
"""Залогировать перехваченное исключение с идентификацией источника.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
module : str
|
||||
Имя модуля (обычно ``__name__``).
|
||||
func : str
|
||||
Имя функции / метода, в котором произошла ошибка.
|
||||
exc : BaseException
|
||||
Перехваченное исключение.
|
||||
"""
|
||||
exc_type = type(exc).__name__
|
||||
if exc.__traceback__ is not None:
|
||||
formatted_traceback = "".join(
|
||||
traceback.format_exception(type(exc), exc, exc.__traceback__)
|
||||
)
|
||||
else:
|
||||
formatted_traceback = f"{exc_type}: {exc}"
|
||||
_error_logger.error(
|
||||
"%s | %s | %s | %s\n%s",
|
||||
module,
|
||||
func,
|
||||
exc_type,
|
||||
exc,
|
||||
formatted_traceback,
|
||||
)
|
||||
Reference in New Issue
Block a user