settings_logging.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. __package__ = 'archivebox.core'
  2. import re
  3. import os
  4. import shutil
  5. import tempfile
  6. import logging
  7. import pydantic
  8. import django.template
  9. from archivebox.config import CONSTANTS
  10. from ..misc.logging import IS_TTY
  11. IGNORABLE_URL_PATTERNS = [
  12. re.compile(r"/.*/?apple-touch-icon.*\.png"),
  13. re.compile(r"/.*/?favicon\.ico"),
  14. re.compile(r"/.*/?robots\.txt"),
  15. re.compile(r"/.*/?.*\.(css|js)\.map"),
  16. re.compile(r"/.*/?.*\.(css|js)\.map"),
  17. re.compile(r"/static/.*"),
  18. re.compile(r"/admin/jsi18n/"),
  19. ]
  20. class NoisyRequestsFilter(logging.Filter):
  21. def filter(self, record) -> bool:
  22. logline = record.getMessage()
  23. # '"GET /api/v1/docs HTTP/1.1" 200 1023'
  24. # '"GET /static/admin/js/SelectFilter2.js HTTP/1.1" 200 15502'
  25. # '"GET /static/admin/js/SelectBox.js HTTP/1.1" 304 0'
  26. # '"GET /admin/jsi18n/ HTTP/1.1" 200 3352'
  27. # '"GET /admin/api/apitoken/0191bbf8-fd5e-0b8c-83a8-0f32f048a0af/change/ HTTP/1.1" 200 28778'
  28. # ignore harmless 404s for the patterns in IGNORABLE_URL_PATTERNS
  29. for pattern in IGNORABLE_URL_PATTERNS:
  30. ignorable_GET_request = re.compile(f'"GET {pattern.pattern} HTTP/.*" (2..|30.|404) .+$', re.I | re.M)
  31. if ignorable_GET_request.match(logline):
  32. return False
  33. ignorable_404_pattern = re.compile(f'Not Found: {pattern.pattern}', re.I | re.M)
  34. if ignorable_404_pattern.match(logline):
  35. return False
  36. return True
  37. class CustomOutboundWebhookLogFormatter(logging.Formatter):
  38. def format(self, record):
  39. result = super().format(record)
  40. return result.replace('HTTP Request: ', 'OutboundWebhook: ')
  41. ERROR_LOG = tempfile.NamedTemporaryFile().name
  42. LOGS_DIR = CONSTANTS.LOGS_DIR
  43. if os.access(LOGS_DIR, os.W_OK) and LOGS_DIR.is_dir():
  44. ERROR_LOG = (LOGS_DIR / 'errors.log')
  45. else:
  46. # historically too many edge cases here around creating log dir w/ correct permissions early on
  47. # if there's an issue on startup, we trash the log and let user figure it out via stdout/stderr
  48. # print(f'[!] WARNING: data/logs dir does not exist. Logging to temp file: {ERROR_LOG}')
  49. pass
  50. LOG_LEVEL_DATABASE = 'WARNING' # if DEBUG else 'WARNING'
  51. LOG_LEVEL_REQUEST = 'WARNING' # if DEBUG else 'WARNING'
  52. SETTINGS_LOGGING = {
  53. "version": 1,
  54. "disable_existing_loggers": False,
  55. "formatters": {
  56. "rich": {
  57. "datefmt": "[%Y-%m-%d %H:%M:%S]",
  58. # "format": "{asctime} {levelname} {module} {name} {message} {username}",
  59. "format": "%(name)s %(message)s",
  60. },
  61. "outbound_webhooks": {
  62. "()": CustomOutboundWebhookLogFormatter,
  63. "datefmt": "[%Y-%m-%d %H:%M:%S]",
  64. },
  65. },
  66. "filters": {
  67. "noisyrequestsfilter": {
  68. "()": NoisyRequestsFilter,
  69. },
  70. "require_debug_false": {
  71. "()": "django.utils.log.RequireDebugFalse",
  72. },
  73. "require_debug_true": {
  74. "()": "django.utils.log.RequireDebugTrue",
  75. },
  76. },
  77. "handlers": {
  78. # "console": {
  79. # "level": "DEBUG",
  80. # 'formatter': 'simple',
  81. # "class": "logging.StreamHandler",
  82. # 'filters': ['noisyrequestsfilter', 'add_extra_logging_attrs'],
  83. # },
  84. "default": {
  85. "class": "rich.logging.RichHandler",
  86. "formatter": "rich",
  87. "level": "DEBUG",
  88. "markup": False,
  89. "rich_tracebacks": IS_TTY,
  90. "filters": ["noisyrequestsfilter"],
  91. "tracebacks_suppress": [
  92. django,
  93. pydantic,
  94. ],
  95. "tracebacks_width": shutil.get_terminal_size((100, 10)).columns - 1,
  96. "tracebacks_word_wrap": False,
  97. "tracebacks_show_locals": False,
  98. },
  99. "logfile": {
  100. "level": "INFO",
  101. "class": "logging.handlers.RotatingFileHandler",
  102. "filename": ERROR_LOG,
  103. "maxBytes": 1024 * 1024 * 25, # 25 MB
  104. "backupCount": 10,
  105. "formatter": "rich",
  106. "filters": ["noisyrequestsfilter"],
  107. },
  108. "outbound_webhooks": {
  109. "class": "rich.logging.RichHandler",
  110. "markup": False,
  111. "rich_tracebacks": True,
  112. "formatter": "outbound_webhooks",
  113. },
  114. # "mail_admins": {
  115. # "level": "ERROR",
  116. # "filters": ["require_debug_false"],
  117. # "class": "django.utils.log.AdminEmailHandler",
  118. # },
  119. "null": {
  120. "class": "logging.NullHandler",
  121. },
  122. },
  123. "root": {
  124. "handlers": ["default", "logfile"],
  125. "level": "INFO",
  126. "formatter": "rich",
  127. },
  128. "loggers": {
  129. "api": {
  130. "handlers": ["default", "logfile"],
  131. "level": "DEBUG",
  132. "propagate": False,
  133. },
  134. "checks": {
  135. "handlers": ["default", "logfile"],
  136. "level": "DEBUG",
  137. "propagate": False,
  138. },
  139. "core": {
  140. "handlers": ["default", "logfile"],
  141. "level": "DEBUG",
  142. "propagate": False,
  143. },
  144. "httpx": {
  145. "handlers": ["outbound_webhooks"],
  146. "level": "INFO",
  147. "formatter": "outbound_webhooks",
  148. "propagate": False,
  149. },
  150. "django": {
  151. "handlers": ["default", "logfile"],
  152. "level": "INFO",
  153. "filters": ["noisyrequestsfilter"],
  154. "propagate": False,
  155. },
  156. "django.utils.autoreload": {
  157. "propagate": False,
  158. "handlers": [],
  159. "level": "ERROR",
  160. },
  161. "django.channels.server": {
  162. # see archivebox.monkey_patches.ModifiedAccessLogGenerator for dedicated daphne server logging settings
  163. "propagate": False,
  164. "handlers": ["default", "logfile"],
  165. "level": "INFO",
  166. "filters": ["noisyrequestsfilter"],
  167. },
  168. "django.server": { # logs all requests (2xx, 3xx, 4xx)
  169. "propagate": False,
  170. "handlers": ["default", "logfile"],
  171. "level": "INFO",
  172. "filters": ["noisyrequestsfilter"],
  173. },
  174. "django.request": { # only logs 4xx and 5xx errors
  175. "propagate": False,
  176. "handlers": ["default", "logfile"],
  177. "level": "ERROR",
  178. "filters": ["noisyrequestsfilter"],
  179. },
  180. "django.db.backends": {
  181. "propagate": False,
  182. "handlers": ["default"],
  183. "level": LOG_LEVEL_DATABASE,
  184. },
  185. },
  186. }