From ae4794626519f58b53c090752a956087af6d7611 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 9 Dec 2025 10:38:15 +0330 Subject: [PATCH] logger --- main/settings.py | 38 ++++++++++++++++++++ utils/exceptions.py | 1 + utils/logs/__init__.py | 4 +++ utils/logs/formatters.py | 77 ++++++++++++++++++++++++++++++++++++++++ utils/logs/handlers.py | 57 +++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 utils/logs/__init__.py create mode 100644 utils/logs/formatters.py create mode 100644 utils/logs/handlers.py diff --git a/main/settings.py b/main/settings.py index 2555941..2b1db7f 100644 --- a/main/settings.py +++ b/main/settings.py @@ -254,6 +254,44 @@ CACHES = { } } +LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str) + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'loki': { + 'class': 'utils.logs.LokiFormatter', # required + }, + }, + + 'handlers': { + 'loki': { + 'level': 'DEBUG', # Log level. Required + 'class': 'utils.logs.LokiHandler', # Required + 'formatter': 'loki', # Loki formatter. Required + 'timeout': 2, # Post request timeout, default is 0.5. Optional + 'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional. + # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional + 'tags': {"app": "ipg"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use. + 'mode': 'thread', # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional. + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + # 'formatter': 'verbose', + }, + }, + 'loggers': { + '': { + 'handlers': ['console', 'loki'], + 'level': 'INFO', + 'propagate': True, + }, + }, +} + PNA_CORPORATION_PIN = config('PNA_CORPORATION_PIN') WALLET_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL') WALLET_RIAL = config('WALLET_RIAL') diff --git a/utils/exceptions.py b/utils/exceptions.py index 28a4f95..1fb3b04 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -5,6 +5,7 @@ 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) 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..284e6a4 --- /dev/null +++ b/utils/logs/handlers.py @@ -0,0 +1,57 @@ +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) + + self._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): + 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" + )