-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathlog.py
125 lines (99 loc) · 4 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
import inspect
import logging
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 ``'\t'`` characters.
"""
@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 = "%(asctime)s │ %(levelname)-8s │ %(indent)s%(function)s: %(message)s"
super().__init__(fmt=fmt)
self.baseline = None
self.cut = None
self.manual_push = 0
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 = IndentFormatter.identify_cut(filenames)
# Inject custom information into the record
record.indent = ' ' * (depth - self.baseline + self.manual_push)
record.function = stack[self.cut].function
# Format the record using custom information
out = logging.Formatter.format(self, record)
# Remove custom information from the record
del record.indent
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
if self.manual_push < 0:
self.manual_push = 0
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 set_up_logging():
"""
Configure logging with some first-run configuration. This method must be
called only once from the main process.
"""
formatter = IndentFormatter()
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=(handler,))
def reset_handler():
"""
Reset the formatter on the handler on the root logger. This causes the next
call to the logger can repopulate them based on the new stack in a new file.
"""
formatter = logging.root.handlers[-1].formatter
if isinstance(formatter, IndentFormatter):
formatter.reset()
def change_indent(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
"""
formatter = logging.root.handlers[-1].formatter
if isinstance(formatter, IndentFormatter):
formatter.delta_indent(delta)