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.

228 lines
8.6 KiB

  1. # coding: utf-8
  2. # License AGPL-3 or later (http://www.gnu.org/licenses/lgpl).
  3. # Copyright 2014 Anybox <http://anybox.fr>
  4. # Copyright 2016 Vauxoo (https://www.vauxoo.com) <info@vauxoo.com>
  5. import errno
  6. import logging
  7. import os
  8. import sys
  9. import tempfile
  10. from cStringIO import StringIO
  11. from datetime import datetime
  12. from openerp import http, sql_db, tools
  13. from openerp.addons.web.controllers.main import content_disposition
  14. from openerp.http import request
  15. from openerp.service.db import dump_db_manifest
  16. from openerp.tools.misc import find_in_path
  17. from ..hooks import CoreProfile as core
  18. _logger = logging.getLogger(__name__)
  19. try:
  20. from pstats_print2list import get_pstats_print2list, print_pstats_list
  21. except ImportError as err: # pragma: no cover
  22. _logger.debug(err)
  23. DFTL_LOG_PATH = '/var/lib/postgresql/%s/main/pg_log/postgresql.log'
  24. PGOPTIONS = (
  25. '-c client_min_messages=notice -c log_min_messages=warning '
  26. '-c log_min_error_statement=error '
  27. '-c log_min_duration_statement=0 -c log_connections=on '
  28. '-c log_disconnections=on -c log_duration=off '
  29. '-c log_error_verbosity=verbose -c log_lock_waits=on '
  30. '-c log_statement=none -c log_temp_files=0 '
  31. )
  32. class Capturing(list):
  33. def __enter__(self):
  34. self._stdout = sys.stdout
  35. sys.stdout = self._stringio = StringIO()
  36. return self
  37. def __exit__(self, *args):
  38. self.extend(self._stringio.getvalue().splitlines())
  39. del self._stringio # free up some memory
  40. sys.stdout = self._stdout
  41. class ProfilerController(http.Controller):
  42. _cp_path = '/web/profiler'
  43. player_state = 'profiler_player_clear'
  44. begin_date = ''
  45. end_date = ''
  46. """Indicate the state(css class) of the player:
  47. * profiler_player_clear
  48. * profiler_player_enabled
  49. * profiler_player_disabled
  50. """
  51. @http.route(['/web/profiler/enable'], type='json', auth="user")
  52. def enable(self):
  53. _logger.info("Enabling")
  54. core.enabled = True
  55. ProfilerController.begin_date = datetime.now().strftime(
  56. "%Y-%m-%d %H:%M:%S")
  57. ProfilerController.player_state = 'profiler_player_enabled'
  58. os.environ['PGOPTIONS'] = PGOPTIONS
  59. self.empty_cursor_pool()
  60. @http.route(['/web/profiler/disable'], type='json', auth="user")
  61. def disable(self, **post):
  62. _logger.info("Disabling")
  63. core.enabled = False
  64. ProfilerController.end_date = datetime.now().strftime(
  65. "%Y-%m-%d %H:%M:%S")
  66. ProfilerController.player_state = 'profiler_player_disabled'
  67. os.environ.pop("PGOPTIONS", None)
  68. self.empty_cursor_pool()
  69. @http.route(['/web/profiler/clear'], type='json', auth="user")
  70. def clear(self, **post):
  71. core.profile.clear()
  72. _logger.info("Cleared stats")
  73. ProfilerController.player_state = 'profiler_player_clear'
  74. ProfilerController.end_date = ''
  75. ProfilerController.begin_date = ''
  76. @http.route(['/web/profiler/dump'], type='http', auth="user")
  77. def dump(self, token, **post):
  78. """Provide the stats as a file download.
  79. Uses a temporary file, because apparently there's no API to
  80. dump stats in a stream directly.
  81. """
  82. exclude_fname = self.get_exclude_fname()
  83. with tools.osutil.tempdir() as dump_dir:
  84. ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
  85. filename = 'openerp_%s' % ts
  86. stats_path = os.path.join(dump_dir, '%s.stats' % filename)
  87. core.profile.dump_stats(stats_path)
  88. _logger.info("Pstats Command:")
  89. params = {'fnames': stats_path, 'sort': 'cumulative', 'limit': 45,
  90. 'exclude_fnames': exclude_fname}
  91. _logger.info(
  92. "fnames=%(fnames)s, sort=%(sort)s,"
  93. " limit=%(limit)s, exclude_fnames=%(exclude_fnames)s", params)
  94. pstats_list = get_pstats_print2list(**params)
  95. with Capturing() as output:
  96. print_pstats_list(pstats_list)
  97. result_path = os.path.join(dump_dir, '%s.txt' % filename)
  98. with open(result_path, "a") as res_file:
  99. for line in output:
  100. res_file.write('%s\n' % line)
  101. # PG_BADGER
  102. self.dump_pgbadger(dump_dir, 'pgbadger_output.txt', request.cr)
  103. t_zip = tempfile.TemporaryFile()
  104. tools.osutil.zip_dir(dump_dir, t_zip, include_dir=False)
  105. t_zip.seek(0)
  106. headers = [
  107. ('Content-Type', 'application/octet-stream; charset=binary'),
  108. ('Content-Disposition', content_disposition(
  109. '%s.zip' % filename))]
  110. _logger.info('Download Profiler zip: %s', t_zip.name)
  111. return request.make_response(
  112. t_zip, headers=headers, cookies={'fileToken': token})
  113. @http.route(['/web/profiler/initial_state'], type='json', auth="user")
  114. def initial_state(self, **post):
  115. user = request.env['res.users'].browse(request.uid)
  116. return {
  117. 'has_player_group': user.has_group(
  118. 'profiler.group_profiler_player'),
  119. 'player_state': ProfilerController.player_state,
  120. }
  121. def dump_pgbadger(self, dir_dump, output, cursor):
  122. pgbadger = find_in_path("pgbadger")
  123. if not pgbadger:
  124. _logger.error("Pgbadger not found")
  125. return
  126. filename = os.path.join(dir_dump, output)
  127. pg_version = dump_db_manifest(cursor)['pg_version']
  128. log_path = os.environ.get('PG_LOG_PATH', DFTL_LOG_PATH % pg_version)
  129. if not os.path.exists(os.path.dirname(filename)):
  130. try:
  131. os.makedirs(os.path.dirname(filename))
  132. except OSError as exc:
  133. # error is different than File exists
  134. if exc.errno != errno.EEXIST:
  135. _logger.error("Folder %s can not be created",
  136. os.path.dirname(filename))
  137. return
  138. _logger.info("Generating PG Badger report.")
  139. exclude_query = self.get_exclude_query()
  140. dbname = cursor.dbname
  141. command = [
  142. pgbadger, '-f', 'stderr', '-T', 'Odoo-Profiler',
  143. '-o', '-', '-d', dbname, '-b', ProfilerController.begin_date,
  144. '-e', ProfilerController.end_date, '--sample', '2',
  145. '--disable-type', '--disable-error', '--disable-hourly',
  146. '--disable-session', '--disable-connection',
  147. '--disable-temporary', '--quiet']
  148. command.extend(exclude_query)
  149. command.append(log_path)
  150. _logger.info("Pgbadger Command:")
  151. _logger.info(command)
  152. result = tools.exec_command_pipe(*command)
  153. with open(filename, 'w') as fw:
  154. fw.write(result[1].read())
  155. _logger.info("Done")
  156. def get_exclude_fname(self):
  157. efnameid = request.env.ref(
  158. 'profiler.default_exclude_fnames_pstas', raise_if_not_found=False)
  159. if not efnameid:
  160. return []
  161. return [os.path.expanduser(path)
  162. for path in efnameid and efnameid.value.strip(',').split(',')
  163. if path]
  164. def get_exclude_query(self):
  165. """Example '^(COPY|COMMIT)'
  166. """
  167. equeryid = request.env.ref(
  168. 'profiler.default_exclude_query_pgbadger',
  169. raise_if_not_found=False)
  170. if not equeryid:
  171. return []
  172. exclude_queries = []
  173. for path in equeryid and equeryid.value.strip(',').split(','):
  174. exclude_queries.extend(
  175. ['--exclude-query', '"^(%s)" ' % path.encode('UTF-8')])
  176. return exclude_queries
  177. def empty_cursor_pool(self):
  178. """This method cleans (rollback) all current transactions over actual
  179. cursor in order to avoid errors with waiting transactions.
  180. - request.cr.rollback()
  181. Also connections on current database's only are closed by the next
  182. statement
  183. - dsn = openerp.sql_db.dsn(request.cr.dbname)
  184. - openerp.sql_db._Pool.close_all(dsn[1])
  185. Otherwise next error will be trigger
  186. 'InterfaceError: connection already closed'
  187. Finally new cursor is assigned to the request object, this cursor will
  188. take the os.environ setted. In this case the os.environ is setted with
  189. all 'PGOPTIONS' required to log all sql transactions in postgres.log
  190. file.
  191. If this method is called one more time, it will create a new cursor and
  192. take the os.environ again, this is usefully if we want to reset
  193. 'PGOPTIONS'
  194. """
  195. request.cr._cnx.reset()
  196. dsn = sql_db.dsn(request.cr.dbname)
  197. sql_db._Pool.close_all(dsn[1])
  198. db = sql_db.db_connect(request.cr.dbname)
  199. request._cr = db.cursor()