mail_logging.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright © 2014 Red Hat, Inc.
  4. #
  5. # This copyrighted material is made available to anyone wishing to use,
  6. # modify, copy, or redistribute it subject to the terms and conditions
  7. # of the GNU General Public License v.2, or (at your option) any later
  8. # version. This program is distributed in the hope that it will be
  9. # useful, but WITHOUT ANY WARRANTY expressed or implied, including the
  10. # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  11. # PURPOSE. See the GNU General Public License for more details. You
  12. # should have received a copy of the GNU General Public License along
  13. # with this program; if not, write to the Free Software Foundation,
  14. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  15. #
  16. # Any Red Hat trademarks that are incorporated in the source
  17. # code or documentation are not subject to the GNU General Public
  18. # License and may only be used or replicated with the express permission
  19. # of Red Hat, Inc.
  20. #
  21. """
  22. Mail handler for logging.
  23. """
  24. from __future__ import absolute_import, unicode_literals
  25. import inspect
  26. import logging
  27. import logging.handlers
  28. import socket
  29. import traceback
  30. import flask
  31. psutil = None
  32. try:
  33. import psutil
  34. except (OSError, ImportError): # pragma: no cover
  35. # We run into issues when trying to import psutil from inside mod_wsgi on
  36. # rhel7. If we hit that here, then just fail quietly.
  37. # https://github.com/jmflinuxtx/kerneltest-harness/pull/17#issuecomment-48007837
  38. pass
  39. def format_callstack():
  40. """Format the callstack to find out the stack trace."""
  41. ind = 0
  42. for ind, frame in enumerate(f[0] for f in inspect.stack()):
  43. if "__name__" not in frame.f_globals:
  44. continue
  45. modname = frame.f_globals["__name__"].split(".")[0]
  46. if modname != "logging":
  47. break
  48. def _format_frame(frame):
  49. """Format the frame."""
  50. return ' File "%s", line %i in %s\n %s' % (frame)
  51. stack = traceback.extract_stack()
  52. stack = stack[:-ind]
  53. return "\n".join([_format_frame(frame) for frame in stack])
  54. class ContextInjector(logging.Filter): # pragma: no cover
  55. """Logging filter that adds context to log records.
  56. Filters are typically used to "filter" log records. They declare a filter
  57. method that can return True or False. Only records with 'True' will
  58. actually be logged.
  59. Here, we somewhat abuse the concept of a filter. We always return true,
  60. but we use the opportunity to hang important contextual information on the
  61. log record to later be used by the logging Formatter. We don't normally
  62. want to see all this stuff in normal log records, but we *do* want to see
  63. it when we are emailed error messages. Seeing an error, but not knowing
  64. which host it comes from, is not that useful.
  65. http://docs.python.org/2/howto/logging-cookbook.html#filters-contextual
  66. This code has been originally written by Ralph Bean for the fedmsg
  67. project:
  68. https://github.com/fedora-infra/fedmsg/
  69. and can be found at:
  70. https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/fedmsg/base/templates/logging.py.j2
  71. """
  72. def filter(self, record):
  73. """Set up additional information on the record object."""
  74. current_process = ContextInjector.get_current_process()
  75. current_hostname = socket.gethostname()
  76. record.host = current_hostname
  77. record.proc = current_process
  78. record.pid = "-"
  79. if not isinstance(current_process, str):
  80. record.pid = current_process.pid
  81. # Be compatible with python-psutil 1.0 and 2.0, 3.0
  82. proc_name = current_process.name
  83. if callable(proc_name):
  84. proc_name = proc_name()
  85. record.proc_name = proc_name
  86. # Be compatible with python-psutil 1.0 and 2.0, 3.0
  87. cmd_line = current_process.cmdline
  88. if callable(cmd_line):
  89. cmd_line = cmd_line()
  90. record.command_line = " ".join(cmd_line)
  91. record.callstack = format_callstack()
  92. try:
  93. record.url = getattr(flask.request, "url", "-")
  94. record.args = getattr(flask.request, "args", "-")
  95. record.form = "-"
  96. record.username = "-"
  97. try:
  98. record.form = dict(flask.request.form)
  99. if "csrf_token" in record.form:
  100. record.form["csrf_token"] = "Was present, is cleaned up"
  101. except RuntimeError:
  102. pass
  103. try:
  104. record.username = flask.g.fas_user.username
  105. except Exception:
  106. pass
  107. except RuntimeError:
  108. # This means we are sending an error email from the worker
  109. record.url = "* Worker *"
  110. record.args = ""
  111. record.form = "-"
  112. record.username = "-"
  113. return True
  114. @staticmethod
  115. def get_current_process():
  116. """Return the current process (PID)."""
  117. if not psutil:
  118. return "Could not import psutil"
  119. return psutil.Process()
  120. MSG_FORMAT = """Process Details
  121. ---------------
  122. host: %(host)s
  123. PID: %(pid)s
  124. name: %(proc_name)s
  125. command: %(command_line)s
  126. Message type: %(levelname)s
  127. Location: %(pathname)s:%(lineno)d
  128. Module: %(module)s
  129. Function: %(funcName)s
  130. Time: %(asctime)s
  131. URL: %(url)s
  132. args: %(args)s
  133. form: %(form)s
  134. user: %(username)s
  135. Message:
  136. --------
  137. %(message)s
  138. Callstack that lead to the logging statement
  139. --------------------------------------------
  140. %(callstack)s
  141. """
  142. def get_mail_handler(smtp_server, mail_admin, from_email):
  143. """Set up the handler sending emails for big exception"""
  144. mail_handler = logging.handlers.SMTPHandler(
  145. smtp_server, from_email, mail_admin, "Pagure error"
  146. )
  147. mail_handler.setFormatter(logging.Formatter(MSG_FORMAT))
  148. mail_handler.setLevel(logging.ERROR)
  149. mail_handler.addFilter(ContextInjector())
  150. return mail_handler