source: deluge/log.py@ 515dbc

2.0.x develop
Last change on this file since 515dbc was 515dbc, checked in by Calum Lind <calumlind+deluge@gmail.com>, 6 years ago

Minor updates to log.py

  • Property mode set to 100644
File size: 11.6 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
4# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
5#
6# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
7# the additional special exception to link portions of this program with the OpenSSL library.
8# See LICENSE for more details.
9#
10
11"""Logging functions"""
12from __future__ import unicode_literals
13
14import inspect
15import logging
16import logging.handlers
17import os
18import sys
19
20from twisted.internet import defer
21from twisted.python.log import PythonLoggingObserver
22
23from deluge import common
24
25__all__ = ('setup_logger', 'set_logger_level', 'get_plugin_logger', 'LOG')
26
27LoggingLoggerClass = logging.getLoggerClass()
28
29if 'dev' in common.get_version():
30 DEFAULT_LOGGING_FORMAT = '%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
31else:
32 DEFAULT_LOGGING_FORMAT = (
33 '%%(asctime)s [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
34 )
35MAX_LOGGER_NAME_LENGTH = 10
36
37
38class Logging(LoggingLoggerClass):
39 def __init__(self, logger_name):
40 super(Logging, self).__init__(logger_name)
41
42 # This makes module name padding increase to the biggest module name
43 # so that logs keep readability.
44 global MAX_LOGGER_NAME_LENGTH
45 if len(logger_name) > MAX_LOGGER_NAME_LENGTH:
46 MAX_LOGGER_NAME_LENGTH = len(logger_name)
47 for handler in logging.getLogger().handlers:
48 handler.setFormatter(
49 logging.Formatter(
50 DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
51 datefmt='%H:%M:%S',
52 )
53 )
54
55 @defer.inlineCallbacks
56 def garbage(self, msg, *args, **kwargs):
57 yield LoggingLoggerClass.log(self, 1, msg, *args, **kwargs)
58
59 @defer.inlineCallbacks
60 def trace(self, msg, *args, **kwargs):
61 yield LoggingLoggerClass.log(self, 5, msg, *args, **kwargs)
62
63 @defer.inlineCallbacks
64 def debug(self, msg, *args, **kwargs):
65 yield LoggingLoggerClass.debug(self, msg, *args, **kwargs)
66
67 @defer.inlineCallbacks
68 def info(self, msg, *args, **kwargs):
69 yield LoggingLoggerClass.info(self, msg, *args, **kwargs)
70
71 @defer.inlineCallbacks
72 def warning(self, msg, *args, **kwargs):
73 yield LoggingLoggerClass.warning(self, msg, *args, **kwargs)
74
75 warn = warning
76
77 @defer.inlineCallbacks
78 def error(self, msg, *args, **kwargs):
79 yield LoggingLoggerClass.error(self, msg, *args, **kwargs)
80
81 @defer.inlineCallbacks
82 def critical(self, msg, *args, **kwargs):
83 yield LoggingLoggerClass.critical(self, msg, *args, **kwargs)
84
85 @defer.inlineCallbacks
86 def exception(self, msg, *args, **kwargs):
87 yield LoggingLoggerClass.exception(self, msg, *args, **kwargs)
88
89 def findCaller(self, stack_info=False): # NOQA: N802
90 f = logging.currentframe().f_back
91 rv = '(unknown file)', 0, '(unknown function)'
92 while hasattr(f, 'f_code'):
93 co = f.f_code
94 filename = os.path.normcase(co.co_filename)
95 if filename in (
96 __file__.replace('.pyc', '.py'),
97 defer.__file__.replace('.pyc', '.py'),
98 ):
99 f = f.f_back
100 continue
101 if common.PY2:
102 rv = (filename, f.f_lineno, co.co_name)
103 else:
104 rv = (filename, f.f_lineno, co.co_name, None)
105 break
106 return rv
107
108
109levels = {
110 'info': logging.INFO,
111 'warn': logging.WARNING,
112 'warning': logging.WARNING,
113 'error': logging.ERROR,
114 'none': logging.CRITICAL,
115 'debug': logging.DEBUG,
116 'trace': 5,
117 'garbage': 1,
118}
119
120
121def setup_logger(
122 level='error',
123 filename=None,
124 filemode='w',
125 logrotate=None,
126 output_stream=sys.stdout,
127 twisted_observer=True,
128):
129 """
130 Sets up the basic logger and if `:param:filename` is set, then it will log
131 to that file instead of stdout.
132
133 Args:
134 level (str): The log level to use (Default: 'error')
135 filename (str, optional): The log filename. Default is None meaning log
136 to terminal
137 filemode (str): The filemode to use when opening the log file
138 logrotate (int, optional): The size of the logfile in bytes when enabling
139 log rotation (Default is None meaning disabled)
140 output_stream (file descriptor): File descriptor to log to if not logging to file
141 twisted_observer (bool): Whether to setup the custom twisted logging observer.
142 """
143 if logging.getLoggerClass() is not Logging:
144 logging.setLoggerClass(Logging)
145 logging.addLevelName(levels['trace'], 'TRACE')
146 logging.addLevelName(levels['garbage'], 'GARBAGE')
147
148 level = levels.get(level, logging.ERROR)
149
150 root_logger = logging.getLogger()
151
152 if filename and logrotate:
153 handler = logging.handlers.RotatingFileHandler(
154 filename, maxBytes=logrotate, backupCount=5, encoding='utf-8'
155 )
156 elif filename and filemode == 'w':
157 handler_cls = logging.FileHandler
158 if not common.windows_check():
159 handler_cls = getattr(
160 logging.handlers, 'WatchedFileHandler', logging.FileHandler
161 )
162 handler = handler_cls(filename, mode=filemode, encoding='utf-8')
163 else:
164 handler = logging.StreamHandler(stream=output_stream)
165
166 handler.setLevel(level)
167
168 formatter = logging.Formatter(
169 DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH, datefmt='%H:%M:%S'
170 )
171
172 handler.setFormatter(formatter)
173
174 # Check for existing handler to prevent duplicate logging.
175 if root_logger.handlers:
176 for handle in root_logger.handlers:
177 if not isinstance(handle, type(handler)):
178 root_logger.addHandler(handler)
179 else:
180 root_logger.addHandler(handler)
181 root_logger.setLevel(level)
182
183 if twisted_observer:
184 twisted_logging = TwistedLoggingObserver()
185 twisted_logging.start()
186
187
188class TwistedLoggingObserver(PythonLoggingObserver):
189 """
190 Custom logging class to fix missing exception tracebacks in log output with new
191 twisted.logger module in twisted version >= 15.2.
192
193 Related twisted bug ticket: https://twistedmatrix.com/trac/ticket/7927
194
195 """
196
197 def __init__(self):
198 PythonLoggingObserver.__init__(self, loggerName='twisted')
199
200 def emit(self, event_dict):
201 log = logging.getLogger(__name__)
202 if 'log_failure' in event_dict:
203 fmt = '%(log_namespace)s \n%(log_failure)s'
204 getattr(LoggingLoggerClass, event_dict['log_level'].name)(
205 log, fmt % (event_dict)
206 )
207 else:
208 PythonLoggingObserver.emit(self, event_dict)
209
210
211def tweak_logging_levels():
212 """This function allows tweaking the logging levels for all or some loggers.
213 This is mostly usefull for developing purposes hence the contents of the
214 file are NOT like regular deluge config file's.
215
216 To use is, create a file named "logging.conf" on your Deluge's config dir
217 with contents like for example:
218 deluge:warn
219 deluge.core:debug
220 deluge.plugin:error
221
222 What the above mean is the logger "deluge" will be set to the WARN level,
223 the "deluge.core" logger will be set to the DEBUG level and the
224 "deluge.plugin" will be set to the ERROR level.
225
226 Remember, one rule per line and this WILL override the setting passed from
227 the command line.
228 """
229 from deluge import configmanager
230
231 logging_config_file = os.path.join(configmanager.get_config_dir(), 'logging.conf')
232 if not os.path.isfile(logging_config_file):
233 return
234 log = logging.getLogger(__name__)
235 log.warning(
236 'logging.conf found! tweaking logging levels from %s', logging_config_file
237 )
238 with open(logging_config_file, 'r') as _file:
239 for line in _file:
240 if line.strip().startswith('#'):
241 continue
242 name, level = line.strip().split(':')
243 if level not in levels:
244 continue
245
246 log.warning('Setting logger "%s" to logging level "%s"', name, level)
247 set_logger_level(level, name)
248
249
250def set_logger_level(level, logger_name=None):
251 """
252 Sets the logger level.
253
254 :param level: str, a string representing the desired level
255 :param logger_name: str, a string representing desired logger name for which
256 the level should change. The default is "None" will tweak
257 the root logger level.
258
259 """
260 logging.getLogger(logger_name).setLevel(levels.get(level, 'error'))
261
262
263def get_plugin_logger(logger_name):
264 import warnings
265
266 stack = inspect.stack()
267 stack.pop(0) # The logging call from this module
268 module_stack = stack.pop(0) # The module that called the log function
269 caller_module = inspect.getmodule(module_stack[0])
270 # In some weird cases caller_module might be None, try to continue
271 caller_module_name = getattr(caller_module, '__name__', '')
272 warnings.warn_explicit(
273 DEPRECATION_WARNING,
274 DeprecationWarning,
275 module_stack[1],
276 module_stack[2],
277 caller_module_name,
278 )
279
280 if 'deluge.plugins.' in logger_name:
281 return logging.getLogger(logger_name)
282 return logging.getLogger('deluge.plugin.%s' % logger_name)
283
284
285DEPRECATION_WARNING = """You seem to be using old style logging on your code, ie:
286 from deluge.log import LOG as log
287
288or:
289 from deluge.log import get_plugin_logger
290
291This has been deprecated in favour of an enhanced logging system and both "LOG"
292and "get_plugin_logger" will be removed on the next major version release of Deluge,
293meaning, code will break, specially plugins.
294If you're seeing this message and you're not the developer of the plugin which
295triggered this warning, please report to it's author.
296If you're the developer, please stop using the above code and instead use:
297
298 import logging
299 log = logging.getLogger(__name__)
300
301
302The above will result in, regarding the "Label" plugin for example a log message similar to:
303 15:33:54 [deluge.plugins.label.core:78 ][INFO ] *** Start Label plugin ***
304
305Triggering code:
306"""
307
308
309class _BackwardsCompatibleLOG(object):
310 def __getattribute__(self, name):
311 import warnings
312
313 logger_name = 'deluge'
314 stack = inspect.stack()
315 stack.pop(0) # The logging call from this module
316 module_stack = stack.pop(0) # The module that called the log function
317 caller_module = inspect.getmodule(module_stack[0])
318 # In some weird cases caller_module might be None, try to continue
319 caller_module_name = getattr(caller_module, '__name__', '')
320 warnings.warn_explicit(
321 DEPRECATION_WARNING,
322 DeprecationWarning,
323 module_stack[1],
324 module_stack[2],
325 caller_module_name,
326 )
327 if caller_module:
328 for member in stack:
329 module = inspect.getmodule(member[0])
330 if not module:
331 continue
332 if module.__name__ in (
333 'deluge.plugins.pluginbase',
334 'deluge.plugins.init',
335 ):
336 logger_name += '.plugin.%s' % caller_module_name
337 # Monkey Patch The Plugin Module
338 caller_module.log = logging.getLogger(logger_name)
339 break
340 else:
341 logging.getLogger(logger_name).warning(
342 "Unable to monkey-patch the calling module's `log` attribute! "
343 'You should really update and rebuild your plugins...'
344 )
345 return getattr(logging.getLogger(logger_name), name)
346
347
348LOG = _BackwardsCompatibleLOG()
Note: See TracBrowser for help on using the repository browser.