Skip to content

Commit 5031f2d

Browse files
authored
Logging overhaul, to solve issues caused by stdout/stderr redirection (#4175)
StreamToLogger is now backed by a filter on the console stream handler, which rejects messages that originated ON stdout/stderr. Additionally, StreamToLogger's capture now only logs after the output is flushed, meaning multi-line tracebacks and the like will be logged as a single message.
1 parent c696ce7 commit 5031f2d

File tree

1 file changed

+56
-38
lines changed

1 file changed

+56
-38
lines changed

src/classes/logger.py

+56-38
Original file line numberDiff line numberDiff line change
@@ -32,73 +32,91 @@
3232

3333
from classes import info
3434

35-
# Dictionary of logging handlers we create, keyed by type
36-
handlers = {}
37-
# Another dictionary of streams we've redirected (stdout, stderr)
38-
streams = {}
39-
4035

4136
class StreamToLogger(object):
4237
"""Custom class to log all stdout and stderr streams (from libopenshot / and other libraries)"""
43-
def __init__(self, logger, log_level=logging.INFO):
44-
self.logger = logger
38+
def __init__(self, parent_stream, log_level=logging.INFO):
39+
self.parent = parent_stream or sys.__stderr__
40+
self.logger = logging.LoggerAdapter(
41+
logging.getLogger('OpenShot.stderr'), {'source': 'stream'})
4542
self.log_level = log_level
46-
self.linebuf = ''
43+
self.logbuf = ''
4744

48-
def write(self, buf):
49-
for line in buf.rstrip().splitlines():
50-
self.logger.log(self.log_level, line.rstrip())
45+
def write(self, text):
46+
self.logbuf += str(text) or ""
47+
self.parent.write(text)
5148

5249
def flush(self):
53-
pass
50+
if self.logbuf.rstrip():
51+
self.logger.log(self.log_level, self.logbuf.rstrip())
52+
self.logbuf = ''
5453

5554
def errors(self):
5655
pass
5756

5857

59-
# Create logger instance
60-
log = logging.Logger('OpenShot')
58+
class StreamFilter(logging.Filter):
59+
"""Filter out lines that originated on the output"""
60+
def filter(self, record):
61+
source = getattr(record, "source", "")
62+
return bool(source != "stream")
63+
6164

6265
# Set up log formatters
6366
template = '%(levelname)s %(module)s: %(message)s'
6467
console_formatter = logging.Formatter(template)
6568
file_formatter = logging.Formatter('%(asctime)s ' + template, datefmt='%H:%M:%S')
6669

67-
# Add normal stderr stream handler
68-
sh = logging.StreamHandler()
69-
sh.setFormatter(console_formatter)
70-
sh.setLevel(info.LOG_LEVEL_CONSOLE)
71-
log.addHandler(sh)
72-
handlers['stream'] = sh
70+
# Configure root logger for minimal logging
71+
logging.basicConfig(level=logging.ERROR)
72+
root_log = logging.getLogger()
73+
74+
# Set up our top-level logging context
75+
log = root_log.getChild('OpenShot')
76+
log.setLevel(info.LOG_LEVEL_FILE)
77+
# Don't pass messages on to root logger
78+
log.propagate = False
7379

74-
# Add rotating file handler
80+
#
81+
# Create rotating file handler
82+
#
7583
fh = logging.handlers.RotatingFileHandler(
76-
os.path.join(info.USER_PATH, 'openshot-qt.log'), encoding="utf-8", maxBytes=25*1024*1024, backupCount=3)
77-
fh.setFormatter(file_formatter)
84+
os.path.join(info.USER_PATH, 'openshot-qt.log'),
85+
encoding="utf-8",
86+
maxBytes=25*1024*1024, backupCount=3)
7887
fh.setLevel(info.LOG_LEVEL_FILE)
88+
fh.setFormatter(file_formatter)
89+
7990
log.addHandler(fh)
80-
handlers['file'] = fh
91+
92+
#
93+
# Create typical stream handler which logs to stderr
94+
#
95+
sh = logging.StreamHandler(sys.stderr)
96+
sh.setLevel(info.LOG_LEVEL_CONSOLE)
97+
sh.setFormatter(console_formatter)
98+
99+
# Filter out redirected output on console, to avoid duplicates
100+
filt = StreamFilter()
101+
sh.addFilter(filt)
102+
103+
log.addHandler(sh)
81104

82105

83106
def reroute_output():
84107
"""Route stdout and stderr to logger (custom handler)"""
85-
if not getattr(sys, 'frozen', False):
86-
# Hang on to the original objects
87-
streams.update({
88-
'stderr': sys.stderr,
89-
'stdout': sys.stdout,
90-
})
91-
# Re-route output streams
92-
handlers['stdout'] = StreamToLogger(log, logging.INFO)
93-
sys.stdout = handlers['stdout']
94-
handlers['stderr'] = StreamToLogger(log, logging.ERROR)
95-
sys.stderr = handlers['stderr']
108+
if (getattr(sys, 'frozen', False)
109+
or sys.stdout != sys.__stdout__):
110+
return
111+
sys.stdout = StreamToLogger(sys.stdout, logging.INFO)
112+
sys.stderr = StreamToLogger(sys.stderr, logging.WARNING)
96113

97114

98115
def set_level_file(level=logging.INFO):
99-
handlers['file'].setLevel(level)
116+
"""Adjust the minimum log level written to our logfile"""
117+
fh.setLevel(level)
100118

101119

102120
def set_level_console(level=logging.INFO):
103-
handlers['stream'].setLevel(level)
104-
121+
"""Adjust the minimum log level for output to the terminal"""
122+
sh.setLevel(level)

0 commit comments

Comments
 (0)