Passed
Push — master ( 56176d...20bec6 )
by Daniil
03:18
created

logger.BaseFormatter.construct_format()   A

Complexity

Conditions 2

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 16
rs 9.7
c 0
b 0
f 0
cc 2
nop 1
1
"""
2
3
Info about logging levels:
4
5
DEBUG: Detailed information, typically of interest only when diagnosing problems.
6
7
INFO: Confirmation that things are working as expected.
8
9
WARNING: An indication that something unexpected happened, or indicative of some
10
problem in the near future (e.g. ‘disk space low’). The software is still working
11
as expected.
12
13
ERROR: Due to a more serious problem, the software has not been able to perform
14
some function.
15
16
CRITICAL: A serious error, indicating that the program itself may be unable to
17
continue running.
18
19
"""
20
21
import logging
22
import os
23
import sys
24
import traceback
25
from datetime import datetime
26
from datetime import timedelta
27
from datetime import timezone
28
from logging import Formatter
29
from logging import Handler
30
from logging import LogRecord
31
32
import requests
33
34
35
def handle_exception(exc_type, exc_value, exc_traceback):
36
    if issubclass(exc_type, KeyboardInterrupt):
37
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
38
        return
39
    logger = Logger().init()
40
    logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
41
42
43
class TelegramHandler(Handler):
44
    def emit(self, record: LogRecord) -> None:
45
        log_entry = self.format(record)
46
        token = os.environ["TG_TOKEN"]
47
        chat_ids = os.environ["TG_CHATS"].split(",")
48
        notifications = False
49
        if record.levelno < 30:
50
            notifications = True
51
        for chat in chat_ids:
52
            requests.get(
53
                f"https://api.telegram.org/bot{token}/sendMessage",
54
                params={
55
                    "chat_id": chat,
56
                    "text": log_entry,
57
                    "parse_mode": "markdown",
58
                    "disable_notification": notifications,
59
                },
60
            )
61
62
63
class BaseFormatter(Formatter):
64
    @staticmethod
65
    def construct_format(record: LogRecord) -> dict:
66
        msg = record.msg
67
        ln = record.levelname
68
        mdl = record.module
69
        timestamp = datetime.utcfromtimestamp(record.created)
70
        ts = (
71
            timestamp.replace(tzinfo=timezone.utc)
72
            .astimezone(tz=timezone(timedelta(hours=3)))
73
            .strftime("%d.%m.%Y %H:%M:%S")
74
        )
75
        if record.exc_info:
76
            trs = traceback.format_exception(*record.exc_info)
77
        else:
78
            trs = None
79
        return {"msg": msg, "ln": ln, "mdl": mdl, "ts": ts, "trs": trs}
80
81
    def format(self, record: LogRecord) -> str:
82
        data = self.construct_format(record)
83
        fmt = f"[{data['ln']}] ({data['mdl']}): {data['ts']} {data['msg']}"
84
        if record.exc_info:
85
            fmt += f"\n{''.join(data['trs'])}"
86
        return fmt
87
88
89
class MarkdownFormatter(BaseFormatter):
90
    def format(self, record: LogRecord) -> str:
91
        data = self.construct_format(record)
92
        fmt = f"*[{data['ln']}]* (`{data['mdl']}`): {data['ts']} {data['msg']}"
93
        if record.exc_info:
94
            fmt += f"""
95
                    ```python
96
{''.join(data['trs'])}
97
                    ```
98
                    """
99
        return fmt
100
101
102
class Logger:
103
    def __init__(self):
104
        self.logger = logging.getLogger(__name__)
105
        if self.logger.hasHandlers():
106
            self.logger.handlers.clear()
107
108
        sys.excepthook = handle_exception
109
110
    def init(self):
111
        self.logger.setLevel("INFO")
112
        console = logging.StreamHandler()
113
        base_formatter = BaseFormatter()
114
        console.setFormatter(base_formatter)
115
        self.logger.addHandler(console)
116
        if "PRODUCTION" in os.environ:
117
            tg = TelegramHandler()
118
            md_formatter = MarkdownFormatter()
119
            tg.setFormatter(md_formatter)
120
            self.logger.addHandler(tg)
121
        return self.logger
122