From 119165f2e1b4c80e7f37817627f44cef02c0f915 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 9 Dec 2025 11:09:08 +0330 Subject: [PATCH] logger --- accounts/settings.py | 39 ++++++++++++++++++++ apps/users/models.py | 4 +-- utils/logs/__init__.py | 4 +++ utils/logs/formatters.py | 77 ++++++++++++++++++++++++++++++++++++++++ utils/logs/handlers.py | 57 +++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 utils/logs/__init__.py create mode 100644 utils/logs/formatters.py create mode 100644 utils/logs/handlers.py diff --git a/accounts/settings.py b/accounts/settings.py index e4af589..8a2bc14 100644 --- a/accounts/settings.py +++ b/accounts/settings.py @@ -282,6 +282,45 @@ 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": "wallet"}, # 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, + }, + }, +} + + from datetime import timedelta from typing import List, Tuple diff --git a/apps/users/models.py b/apps/users/models.py index 99b7e16..3477749 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -154,8 +154,8 @@ class User(AbstractUser): def set_otp(self): if self.otp_expire and (self.otp_expire + timedelta(seconds=MAX_OTP_VALID_DURATION)) > timezone.now(): raise Exception('otp expire time not reached.') - self._otp = ''.join(random.choice('0123456789') for _ in range(5)) - # self._otp = '77501' + # self._otp = ''.join(random.choice('0123456789') for _ in range(5)) + self._otp = '77501' self.otp = make_password(self._otp) self.otp_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION) self.otp_try = 0 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" + )