-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathlog.py
168 lines (141 loc) · 5.73 KB
/
log.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# Standard library
import inspect
import logging
SUCCESS = logging.INFO + 1
class IndentFormatter(logging.Formatter):
"""
Format the given log messages with proper indentation based on the stack
depth of the code invoking the logger. This removes the need for manual
indentation using tab characters.
"""
# https://en.wikipedia.org/wiki/ANSI_escape_code
color_map = { # ............. Level ## Color ##
logging.CRITICAL: 31, # . CRITICAL 50 red 31
logging.ERROR: 31, # .... ERROR 40 red 31
logging.WARNING: 33, # .. WARNING 30 yellow 33
SUCCESS: 32, # .......... SUCCESS 21 green 32
logging.INFO: 34, # ..... INFO 20 blue 34
logging.DEBUG: 35, # .... DEBUG 10 magenta 35
}
@staticmethod
def identify_cut(filenames):
"""
Identify the depth at which the invoking function can be located. The
invoking function would be the first occurrence of a file just after
all stack filenames from within Python libs itself.
@param filenames: the names of all files from which logs were pushed
@return: the index of the filename from which the logger was called
"""
lib_string = "lib/python"
lib_started = False
for index, filename in enumerate(filenames):
if not lib_started and lib_string in filename:
lib_started = True
if lib_started and lib_string not in filename:
return index
def __init__(self):
"""
Initialise the formatter with the fixed log format. The format is
intentionally minimal to get clean and readable logs.
"""
fmt = "%(message)s"
super().__init__(fmt=fmt)
self.baseline = None
self.cut = None
self.manual_push = 0
def update_format(self, record):
"""
Update the format string based on the log level of the record.
@param record: the record based on whose level to update the formatting
"""
prefix = "\u001b["
color = f"{prefix}{self.color_map[record.levelno]}m"
bold = f"{prefix}1m"
gray = f"{prefix}1m{prefix}30m"
reset = f"{prefix}0m"
self._style._fmt = (
f"%(asctime)s"
f" {gray}│{reset} {color}%(levelname)-8s{reset} {gray}│{reset} "
)
if hasattr(record, "function"):
self._style._fmt += (
f"{gray}%(indent)s{reset}"
f"{bold}%(function)s{reset}{gray}:{reset}"
" %(message)s"
)
else:
self._style._fmt += "%(indent)s%(message)s"
def format(self, record):
"""
Format the log message with additional data extracted from the stack.
@param record: the log record to format with this formatter
@return: the formatted log record
"""
stack = inspect.stack(context=0)
depth = len(stack)
if self.baseline is None:
self.baseline = depth
if self.cut is None:
filenames = map(lambda x: x.filename, stack)
self.cut = self.identify_cut(filenames)
# Inject custom information into the record
record.indent = "." * (depth - self.baseline + self.manual_push)
if depth > self.cut:
record.function = stack[self.cut].function
# Format the record using custom information
self.update_format(record)
out = super().format(record)
# Remove custom information from the record
del record.indent
if hasattr(record, "function"):
del record.function
return out
def delta_indent(self, delta=1):
"""
Change the manual push value by the given number of steps. Increasing
the value indents the logs and decreasing it de-indents them.
@param delta: the number of steps by which to indent/de-indent the logs
"""
self.manual_push += delta
def reset(self):
"""
Reset the baseline and cut attributes so that the next call to the
logger can repopulate them to the new values for the particular file.
"""
self.baseline = None
self.cut = None
self.manual_push = 0
def setup_logger():
"""
Configure RootLogger. This method must be called only once from the main
script (not from modules/libraries included by that script).
"""
def log_success_class(self, message, *args, **kwargs):
if self.isEnabledFor(SUCCESS):
# The 'args' below (instead of '*args') is correct
self._log(SUCCESS, message, args, **kwargs)
def log_success_root(message, *args, **kwargs):
logging.log(SUCCESS, message, *args, **kwargs)
def change_indent_class(self, delta=1):
"""
Indent the output of the logger by the given number of steps. If
positive, the indentation increases and if negative, it decreases.
@param delta: the number of steps by which to indent/de-indent the logs
"""
handlers = self.handlers
if len(handlers) > 0:
formatter = handlers[-1].formatter
if isinstance(formatter, IndentFormatter):
formatter.delta_indent(delta)
logging.addLevelName(SUCCESS, "SUCCESS")
setattr(logging.getLoggerClass(), "success", log_success_class)
setattr(logging, "success", log_success_root)
setattr(logging.getLoggerClass(), "change_indent", change_indent_class)
formatter = IndentFormatter()
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger = logging.root
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
__all__ = ["setup_logger"]