netsvc.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import contextlib
  4. import logging
  5. import logging.handlers
  6. import os
  7. import platform
  8. import pprint
  9. import sys
  10. import threading
  11. import time
  12. import traceback
  13. import warnings
  14. import werkzeug.serving
  15. from . import release
  16. from . import sql_db
  17. from . import tools
  18. _logger = logging.getLogger(__name__)
  19. def log(logger, level, prefix, msg, depth=None):
  20. indent=''
  21. indent_after=' '*len(prefix)
  22. for line in (prefix + pprint.pformat(msg, depth=depth)).split('\n'):
  23. logger.log(level, indent+line)
  24. indent=indent_after
  25. class WatchedFileHandler(logging.handlers.WatchedFileHandler):
  26. def __init__(self, filename):
  27. self.errors = None # py38
  28. super().__init__(filename)
  29. # Unfix bpo-26789, in case the fix is present
  30. self._builtin_open = None
  31. def _open(self):
  32. return open(self.baseFilename, self.mode, encoding=self.encoding, errors=self.errors)
  33. class PostgreSQLHandler(logging.Handler):
  34. """ PostgreSQL Logging Handler will store logs in the database, by default
  35. the current database, can be set using --log-db=DBNAME
  36. """
  37. def emit(self, record):
  38. ct = threading.current_thread()
  39. ct_db = getattr(ct, 'dbname', None)
  40. dbname = tools.config['log_db'] if tools.config['log_db'] and tools.config['log_db'] != '%d' else ct_db
  41. if not dbname:
  42. return
  43. with contextlib.suppress(Exception), tools.mute_logger('odoo.sql_db'), sql_db.db_connect(dbname, allow_uri=True).cursor() as cr:
  44. # preclude risks of deadlocks
  45. cr.execute("SET LOCAL statement_timeout = 1000")
  46. msg = tools.ustr(record.msg)
  47. if record.args:
  48. msg = msg % record.args
  49. traceback = getattr(record, 'exc_text', '')
  50. if traceback:
  51. msg = "%s\n%s" % (msg, traceback)
  52. # we do not use record.levelname because it may have been changed by ColoredFormatter.
  53. levelname = logging.getLevelName(record.levelno)
  54. val = ('server', ct_db, record.name, levelname, msg, record.pathname, record.lineno, record.funcName)
  55. cr.execute("""
  56. INSERT INTO ir_logging(create_date, type, dbname, name, level, message, path, line, func)
  57. VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s)
  58. """, val)
  59. BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
  60. #The background is set with 40 plus the number of the color, and the foreground with 30
  61. #These are the sequences needed to get colored output
  62. RESET_SEQ = "\033[0m"
  63. COLOR_SEQ = "\033[1;%dm"
  64. BOLD_SEQ = "\033[1m"
  65. COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
  66. LEVEL_COLOR_MAPPING = {
  67. logging.DEBUG: (BLUE, DEFAULT),
  68. logging.INFO: (GREEN, DEFAULT),
  69. logging.WARNING: (YELLOW, DEFAULT),
  70. logging.ERROR: (RED, DEFAULT),
  71. logging.CRITICAL: (WHITE, RED),
  72. }
  73. class PerfFilter(logging.Filter):
  74. def format_perf(self, query_count, query_time, remaining_time):
  75. return ("%d" % query_count, "%.3f" % query_time, "%.3f" % remaining_time)
  76. def filter(self, record):
  77. if hasattr(threading.current_thread(), "query_count"):
  78. query_count = threading.current_thread().query_count
  79. query_time = threading.current_thread().query_time
  80. perf_t0 = threading.current_thread().perf_t0
  81. remaining_time = time.time() - perf_t0 - query_time
  82. record.perf_info = '%s %s %s' % self.format_perf(query_count, query_time, remaining_time)
  83. delattr(threading.current_thread(), "query_count")
  84. else:
  85. record.perf_info = "- - -"
  86. return True
  87. class ColoredPerfFilter(PerfFilter):
  88. def format_perf(self, query_count, query_time, remaining_time):
  89. def colorize_time(time, format, low=1, high=5):
  90. if time > high:
  91. return COLOR_PATTERN % (30 + RED, 40 + DEFAULT, format % time)
  92. if time > low:
  93. return COLOR_PATTERN % (30 + YELLOW, 40 + DEFAULT, format % time)
  94. return format % time
  95. return (
  96. colorize_time(query_count, "%d", 100, 1000),
  97. colorize_time(query_time, "%.3f", 0.1, 3),
  98. colorize_time(remaining_time, "%.3f", 1, 5)
  99. )
  100. class DBFormatter(logging.Formatter):
  101. def format(self, record):
  102. record.pid = os.getpid()
  103. record.dbname = getattr(threading.current_thread(), 'dbname', '?')
  104. return logging.Formatter.format(self, record)
  105. class ColoredFormatter(DBFormatter):
  106. def format(self, record):
  107. fg_color, bg_color = LEVEL_COLOR_MAPPING.get(record.levelno, (GREEN, DEFAULT))
  108. record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
  109. return DBFormatter.format(self, record)
  110. _logger_init = False
  111. def init_logger():
  112. global _logger_init
  113. if _logger_init:
  114. return
  115. _logger_init = True
  116. old_factory = logging.getLogRecordFactory()
  117. def record_factory(*args, **kwargs):
  118. record = old_factory(*args, **kwargs)
  119. record.perf_info = ""
  120. return record
  121. logging.setLogRecordFactory(record_factory)
  122. # enable deprecation warnings (disabled by default)
  123. warnings.simplefilter('default', category=DeprecationWarning)
  124. # ignore deprecation warnings from invalid escape (there's a ton and it's
  125. # pretty likely a super low-value signal)
  126. warnings.filterwarnings('ignore', r'^invalid escape sequence \'?\\.', category=DeprecationWarning)
  127. if sys.version_info[:2] == (3, 9):
  128. # recordsets are both sequence and set so trigger warning despite no issue
  129. # Only applies to 3.9 as it was fixed in 3.10 see https://bugs.python.org/issue42470
  130. warnings.filterwarnings('ignore', r'^Sampling from a set', category=DeprecationWarning, module='odoo')
  131. # https://github.com/urllib3/urllib3/issues/2680
  132. warnings.filterwarnings('ignore', r'^\'urllib3.contrib.pyopenssl\' module is deprecated.+', category=DeprecationWarning)
  133. # ofxparse use an html parser to parse ofx xml files and triggers a warning since bs4 4.11.0
  134. # https://github.com/jseutter/ofxparse/issues/170
  135. try:
  136. from bs4 import XMLParsedAsHTMLWarning
  137. warnings.filterwarnings('ignore', category=XMLParsedAsHTMLWarning)
  138. except ImportError:
  139. pass
  140. # ignore a bunch of warnings we can't really fix ourselves
  141. for module in [
  142. 'babel.util', # deprecated parser module, no release yet
  143. 'zeep.loader',# zeep using defusedxml.lxml
  144. 'reportlab.lib.rl_safe_eval',# reportlab importing ABC from collections
  145. 'ofxparse',# ofxparse importing ABC from collections
  146. 'astroid', # deprecated imp module (fixed in 2.5.1)
  147. 'requests_toolbelt', # importing ABC from collections (fixed in 0.9)
  148. 'firebase_admin', # deprecated method_whitelist
  149. ]:
  150. warnings.filterwarnings('ignore', category=DeprecationWarning, module=module)
  151. # the SVG guesser thing always compares str and bytes, ignore it
  152. warnings.filterwarnings('ignore', category=BytesWarning, module='odoo.tools.image')
  153. # reportlab does a bunch of bytes/str mixing in a hashmap
  154. warnings.filterwarnings('ignore', category=BytesWarning, module='reportlab.platypus.paraparser')
  155. from .tools.translate import resetlocale
  156. resetlocale()
  157. # create a format for log messages and dates
  158. format = '%(asctime)s %(pid)s %(levelname)s %(dbname)s %(name)s: %(message)s %(perf_info)s'
  159. # Normal Handler on stderr
  160. handler = logging.StreamHandler()
  161. if tools.config['syslog']:
  162. # SysLog Handler
  163. if os.name == 'nt':
  164. handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version))
  165. elif platform.system() == 'Darwin':
  166. handler = logging.handlers.SysLogHandler('/var/run/log')
  167. else:
  168. handler = logging.handlers.SysLogHandler('/dev/log')
  169. format = '%s %s' % (release.description, release.version) \
  170. + ':%(dbname)s:%(levelname)s:%(name)s:%(message)s'
  171. elif tools.config['logfile']:
  172. # LogFile Handler
  173. logf = tools.config['logfile']
  174. try:
  175. # We check we have the right location for the log files
  176. dirname = os.path.dirname(logf)
  177. if dirname and not os.path.isdir(dirname):
  178. os.makedirs(dirname)
  179. if os.name == 'posix':
  180. handler = WatchedFileHandler(logf)
  181. else:
  182. handler = logging.FileHandler(logf)
  183. except Exception:
  184. sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
  185. # Check that handler.stream has a fileno() method: when running OpenERP
  186. # behind Apache with mod_wsgi, handler.stream will have type mod_wsgi.Log,
  187. # which has no fileno() method. (mod_wsgi.Log is what is being bound to
  188. # sys.stderr when the logging.StreamHandler is being constructed above.)
  189. def is_a_tty(stream):
  190. return hasattr(stream, 'fileno') and os.isatty(stream.fileno())
  191. if os.name == 'posix' and isinstance(handler, logging.StreamHandler) and (is_a_tty(handler.stream) or os.environ.get("ODOO_PY_COLORS")):
  192. formatter = ColoredFormatter(format)
  193. perf_filter = ColoredPerfFilter()
  194. else:
  195. formatter = DBFormatter(format)
  196. perf_filter = PerfFilter()
  197. werkzeug.serving._log_add_style = False
  198. handler.setFormatter(formatter)
  199. logging.getLogger().addHandler(handler)
  200. logging.getLogger('werkzeug').addFilter(perf_filter)
  201. if tools.config['log_db']:
  202. db_levels = {
  203. 'debug': logging.DEBUG,
  204. 'info': logging.INFO,
  205. 'warning': logging.WARNING,
  206. 'error': logging.ERROR,
  207. 'critical': logging.CRITICAL,
  208. }
  209. postgresqlHandler = PostgreSQLHandler()
  210. postgresqlHandler.setLevel(int(db_levels.get(tools.config['log_db_level'], tools.config['log_db_level'])))
  211. logging.getLogger().addHandler(postgresqlHandler)
  212. # Configure loggers levels
  213. pseudo_config = PSEUDOCONFIG_MAPPER.get(tools.config['log_level'], [])
  214. logconfig = tools.config['log_handler']
  215. logging_configurations = DEFAULT_LOG_CONFIGURATION + pseudo_config + logconfig
  216. for logconfig_item in logging_configurations:
  217. loggername, level = logconfig_item.strip().split(':')
  218. level = getattr(logging, level, logging.INFO)
  219. logger = logging.getLogger(loggername)
  220. logger.setLevel(level)
  221. for logconfig_item in logging_configurations:
  222. _logger.debug('logger level set: "%s"', logconfig_item)
  223. DEFAULT_LOG_CONFIGURATION = [
  224. 'odoo.http.rpc.request:INFO',
  225. 'odoo.http.rpc.response:INFO',
  226. ':INFO',
  227. ]
  228. PSEUDOCONFIG_MAPPER = {
  229. 'debug_rpc_answer': ['odoo:DEBUG', 'odoo.sql_db:INFO', 'odoo.http.rpc:DEBUG'],
  230. 'debug_rpc': ['odoo:DEBUG', 'odoo.sql_db:INFO', 'odoo.http.rpc.request:DEBUG'],
  231. 'debug': ['odoo:DEBUG', 'odoo.sql_db:INFO'],
  232. 'debug_sql': ['odoo.sql_db:DEBUG'],
  233. 'info': [],
  234. 'runbot': ['odoo:RUNBOT', 'werkzeug:WARNING'],
  235. 'warn': ['odoo:WARNING', 'werkzeug:WARNING'],
  236. 'error': ['odoo:ERROR', 'werkzeug:ERROR'],
  237. 'critical': ['odoo:CRITICAL', 'werkzeug:CRITICAL'],
  238. }
  239. logging.RUNBOT = 25
  240. logging.addLevelName(logging.RUNBOT, "INFO") # displayed as info in log
  241. logging.captureWarnings(True)
  242. # must be after `loggin.captureWarnings` so we override *that* instead of the
  243. # other way around
  244. showwarning = warnings.showwarning
  245. IGNORE = {
  246. 'Comparison between bytes and int', # a.foo != False or some shit, we don't care
  247. }
  248. def showwarning_with_traceback(message, category, filename, lineno, file=None, line=None):
  249. if category is BytesWarning and message.args[0] in IGNORE:
  250. return
  251. # find the stack frame matching (filename, lineno)
  252. filtered = []
  253. for frame in traceback.extract_stack():
  254. if 'importlib' not in frame.filename:
  255. filtered.append(frame)
  256. if frame.filename == filename and frame.lineno == lineno:
  257. break
  258. return showwarning(
  259. message, category, filename, lineno,
  260. file=file,
  261. line=''.join(traceback.format_list(filtered))
  262. )
  263. warnings.showwarning = showwarning_with_traceback
  264. def runbot(self, message, *args, **kws):
  265. self.log(logging.RUNBOT, message, *args, **kws)
  266. logging.Logger.runbot = runbot