diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/exceptions.py b/utils/exceptions.py new file mode 100644 index 0000000..1fb3b04 --- /dev/null +++ b/utils/exceptions.py @@ -0,0 +1,51 @@ +import logging + +from rest_framework.exceptions import APIException +from rest_framework.views import exception_handler as drf_exception_handler + +logger = logging.getLogger(__name__) +def exception_handler(exc, context): + logger.exception(exc) + # پاسخ پیش‌فرض DRF را دریافت می‌کنیم + response = drf_exception_handler(exc, context) + + if response is not None: + # ساختار دلخواه خود را تعریف می‌کنیم + response_data = { + 'success': False, + 'status_code': response.status_code, + 'message': 'An error occurred', + 'details': {} + } + + if isinstance(exc, APIException): + response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" + response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} + + response.data = response_data + + return response + + +# در فایل middleware.py +from django.http import JsonResponse + +class ErrorMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + return response + + def process_exception(self, request, exception): + logger.exception(exception) + return JsonResponse( + { + "success": False, + "status_code": 500, + "message": "Internal server error", + "details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود. + }, + status=500 + ) \ No newline at end of file diff --git a/utils/logs/__init__.py b/utils/logs/__init__.py new file mode 100644 index 0000000..5afab06 --- /dev/null +++ b/utils/logs/__init__.py @@ -0,0 +1,4 @@ +from .formatters import LokiFormatter +from .handlers import LokiHandler + +__all__ = ['LokiHandler', 'LokiFormatter'] \ No newline at end of file diff --git a/utils/logs/formatters.py b/utils/logs/formatters.py new file mode 100644 index 0000000..def86d5 --- /dev/null +++ b/utils/logs/formatters.py @@ -0,0 +1,77 @@ +import logging +import socket +from datetime import datetime +from logging import BASIC_FORMAT + + +class LokiFormatter(logging.Formatter): + asctime_search = "%(asctime)" + tz = "UTC" + source = "Loki" + src_host = "localhost" + tags = {} + + def __init__(self, fmt, dfmt, style, fqdn=False): + super(LokiFormatter, self).__init__() + + self.fmt = fmt or BASIC_FORMAT + self.dfmt = dfmt or "%Y-%m-%d %H:%M:%S" + self.style = style + + if fqdn: + self.host = socket.getfqdn() + else: + self.host = socket.gethostname() + + def format_timestamp(self, time): + return str(int(time * 10**9)) + + def usesTime(self): + return self.fmt.find(self.asctime_search) >= 0 + + def formatMessage(self, record): + try: + return self.fmt % record.__dict__ + except KeyError as e: + raise ValueError("Formatting field not found in record: %s" % e) + + def format(self, record): + record.message = record.getMessage() + + if self.usesTime(): + record.asctime = self.formatTime(record, self.dfmt) + + message = self.formatMessage(record) + + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + if record.exc_text: + if message[-1:] != "\n": + message = message + "\n" + message = message + record.exc_text + + if record.stack_info: + if message[-1:] != "\n": + message = message + "\n" + + message = message + self.formatStack(record.stack_info) + + message = { + "streams": [ + { + "stream": { + **self.tags, + }, + "values": [ + [ + self.format_timestamp(record.created), + message, + ] + ], + } + ] + } + + return message diff --git a/utils/logs/handlers.py b/utils/logs/handlers.py new file mode 100644 index 0000000..a647255 --- /dev/null +++ b/utils/logs/handlers.py @@ -0,0 +1,58 @@ +import logging +import sys +from threading import Thread + +import requests + + +class LokiHandler(logging.Handler): + def __init__( + self, + timeout=0.5, + url="http://localhost:3100/loki/api/v1/push", + auth=None, + tags={}, + mode="sync", + ): + super(LokiHandler, self).__init__() + + self._url = url + self._timeout = timeout + self._auth = auth + self._tags = tags + + self._mode = mode + + def emit(self, record): + try: + payload = self.formatter.format(record) + + _push_message( + self._url, json=payload, timeout=self._timeout, auth=self._auth + ) + except requests.exceptions.ReadTimeout: + sys.stderr.write("Loki connection timed out\n") + except Exception as e: + sys.stderr.write(f"Loki connection failed with: {e}\n") + + def setFormatter(self, fmt): + fmt.tags = self._tags + + self.formatter = fmt + + def _push_message(self, *args, **kwargs): + print('hehe') + if self._mode == "sync": + return _push_message(*args, **kwargs) + + if self._mode == "thread": + return Thread(target=_push_message, args=args, kwargs=kwargs).start() + + +def _push_message(*args, **kwargs): + response = requests.post(*args, **kwargs) + + if response.status_code != 204: + sys.stderr.write( + f"Got status {response.status_code} from loki with message: {response.text}\n" + ) diff --git a/wallet/settings.py b/wallet/settings.py index b85b9cd..31cabab 100644 --- a/wallet/settings.py +++ b/wallet/settings.py @@ -67,6 +67,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', + 'utils.exceptions.ErrorMiddleware', ] @@ -111,6 +112,7 @@ REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'PAGE_SIZE': 50, "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + 'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler', } SPECTACULAR_SETTINGS = { @@ -195,7 +197,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Asia/Tehran' USE_I18N = True