gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

331 lines
14KB

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