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"""
|
---|
12 | from __future__ import unicode_literals
|
---|
13 |
|
---|
14 | import inspect
|
---|
15 | import logging
|
---|
16 | import logging.handlers
|
---|
17 | import os
|
---|
18 | import sys
|
---|
19 |
|
---|
20 | from twisted.internet import defer
|
---|
21 | from twisted.python.log import PythonLoggingObserver
|
---|
22 |
|
---|
23 | from deluge import common
|
---|
24 |
|
---|
25 | __all__ = ('setup_logger', 'set_logger_level', 'get_plugin_logger', 'LOG')
|
---|
26 |
|
---|
27 | LoggingLoggerClass = logging.getLoggerClass()
|
---|
28 |
|
---|
29 | if 'dev' in common.get_version():
|
---|
30 | DEFAULT_LOGGING_FORMAT = '%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
|
---|
31 | else:
|
---|
32 | DEFAULT_LOGGING_FORMAT = (
|
---|
33 | '%%(asctime)s [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
|
---|
34 | )
|
---|
35 | MAX_LOGGER_NAME_LENGTH = 10
|
---|
36 |
|
---|
37 |
|
---|
38 | class 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, *args, **kwargs): # 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 |
|
---|
109 | levels = {
|
---|
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 |
|
---|
121 | def 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 |
|
---|
188 | class 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 |
|
---|
211 | def 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 |
|
---|
250 | def 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 |
|
---|
263 | def 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 |
|
---|
285 | DEPRECATION_WARNING = """You seem to be using old style logging on your code, ie:
|
---|
286 | from deluge.log import LOG as log
|
---|
287 |
|
---|
288 | or:
|
---|
289 | from deluge.log import get_plugin_logger
|
---|
290 |
|
---|
291 | This has been deprecated in favour of an enhanced logging system and both "LOG"
|
---|
292 | and "get_plugin_logger" will be removed on the next major version release of Deluge,
|
---|
293 | meaning, code will break, specially plugins.
|
---|
294 | If you're seeing this message and you're not the developer of the plugin which
|
---|
295 | triggered this warning, please report to it's author.
|
---|
296 | If you're the developer, please stop using the above code and instead use:
|
---|
297 |
|
---|
298 | import logging
|
---|
299 | log = logging.getLogger(__name__)
|
---|
300 |
|
---|
301 |
|
---|
302 | The 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 |
|
---|
305 | Triggering code:
|
---|
306 | """
|
---|
307 |
|
---|
308 |
|
---|
309 | class _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 |
|
---|
348 | LOG = _BackwardsCompatibleLOG()
|
---|