##// END OF EJS Templates
exc_tracking: always nice format tb for core exceptions
super-admin -
r5127:23926495 default
parent child Browse files
Show More
@@ -48,7 +48,7 b' from rhodecode.lib.middleware.appenlight'
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.utils2 import AttributeDict
50 from rhodecode.lib.utils2 import AttributeDict
51 from rhodecode.lib.exc_tracking import store_exception
51 from rhodecode.lib.exc_tracking import store_exception, format_exc
52 from rhodecode.subscribers import (
52 from rhodecode.subscribers import (
53 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 write_metadata_if_needed, write_usage_data)
54 write_metadata_if_needed, write_usage_data)
@@ -190,7 +190,6 b' def not_found_view(request):'
190 def error_handler(exception, request):
190 def error_handler(exception, request):
191 import rhodecode
191 import rhodecode
192 from rhodecode.lib import helpers
192 from rhodecode.lib import helpers
193 from rhodecode.lib.utils2 import str2bool
194
193
195 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
194 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
196
195
@@ -202,8 +201,10 b' def error_handler(exception, request):'
202 base_response = VCSServerUnavailable()
201 base_response = VCSServerUnavailable()
203
202
204 if is_http_error(base_response):
203 if is_http_error(base_response):
205 log.exception(
204 traceback_info = format_exc(request.exc_info)
206 'error occurred handling this request for path: %s', request.path)
205 log.error(
206 'error occurred handling this request for path: %s, \n%s',
207 request.path, traceback_info)
207
208
208 error_explanation = base_response.explanation or str(base_response)
209 error_explanation = base_response.explanation or str(base_response)
209 if base_response.status_code == 404:
210 if base_response.status_code == 404:
@@ -16,6 +16,7 b''
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import io
19 import os
20 import os
20 import time
21 import time
21 import sys
22 import sys
@@ -29,19 +30,18 b' import glob'
29 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
30
31
31 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
32 global_prefix = 'rhodecode'
33 global_prefix = "rhodecode"
33 exc_store_dir_name = 'rc_exception_store_v1'
34 exc_store_dir_name = "rc_exception_store_v1"
34
35
35
36
36 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
37 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
37
38 data = {
38 data = {
39 'version': 'v1',
39 "version": "v1",
40 'exc_id': exc_id,
40 "exc_id": exc_id,
41 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
41 "exc_utc_date": datetime.datetime.utcnow().isoformat(),
42 'exc_timestamp': repr(time.time()),
42 "exc_timestamp": repr(time.time()),
43 'exc_message': tb,
43 "exc_message": tb,
44 'exc_type': exc_type,
44 "exc_type": exc_type,
45 }
45 }
46 if extra_data:
46 if extra_data:
47 data.update(extra_data)
47 data.update(extra_data)
@@ -51,69 +51,27 b' def exc_serialize(exc_id, tb, exc_type, '
51 def exc_unserialize(tb):
51 def exc_unserialize(tb):
52 return msgpack.unpackb(tb)
52 return msgpack.unpackb(tb)
53
53
54
54 _exc_store = None
55 _exc_store = None
55
56
56
57
57 def get_exc_store():
58 def maybe_send_exc_email(exc_id, exc_type_name, send_email):
58 """
59 from pyramid.threadlocal import get_current_request
59 Get and create exception store if it's not existing
60 """
61 global _exc_store
62 import rhodecode as app
60 import rhodecode as app
63
61
64 if _exc_store is not None:
65 # quick global cache
66 return _exc_store
67
68 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
69 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
70
71 _exc_store_path = os.path.abspath(_exc_store_path)
72 if not os.path.isdir(_exc_store_path):
73 os.makedirs(_exc_store_path)
74 log.debug('Initializing exceptions store at %s', _exc_store_path)
75 _exc_store = _exc_store_path
76
77 return _exc_store_path
78
79
80 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
81 """
82 Low level function to store exception in the exception tracker
83 """
84 from pyramid.threadlocal import get_current_request
85 import rhodecode as app
86 request = get_current_request()
62 request = get_current_request()
87 extra_data = {}
88 # NOTE(marcink): store request information into exc_data
89 if request:
90 extra_data['client_address'] = getattr(request, 'client_addr', '')
91 extra_data['user_agent'] = getattr(request, 'user_agent', '')
92 extra_data['method'] = getattr(request, 'method', '')
93 extra_data['url'] = getattr(request, 'url', '')
94
95 exc_store_path = get_exc_store()
96 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name, extra_data=extra_data)
97
98 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
99 if not os.path.isdir(exc_store_path):
100 os.makedirs(exc_store_path)
101 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
102 with open(stored_exc_path, 'wb') as f:
103 f.write(exc_data)
104 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
105
63
106 if send_email is None:
64 if send_email is None:
107 # NOTE(marcink): read app config unless we specify explicitly
65 # NOTE(marcink): read app config unless we specify explicitly
108 send_email = app.CONFIG.get('exception_tracker.send_email', False)
66 send_email = app.CONFIG.get("exception_tracker.send_email", False)
109
67
110 mail_server = app.CONFIG.get('smtp_server') or None
68 mail_server = app.CONFIG.get("smtp_server") or None
111 send_email = send_email and mail_server
69 send_email = send_email and mail_server
112 if send_email and request:
70 if send_email and request:
113 try:
71 try:
114 send_exc_email(request, exc_id, exc_type_name)
72 send_exc_email(request, exc_id, exc_type_name)
115 except Exception:
73 except Exception:
116 log.exception('Failed to send exception email')
74 log.exception("Failed to send exception email")
117 exc_info = sys.exc_info()
75 exc_info = sys.exc_info()
118 store_exception(id(exc_info), exc_info, send_email=False)
76 store_exception(id(exc_info), exc_info, send_email=False)
119
77
@@ -126,41 +84,164 b' def send_exc_email(request, exc_id, exc_'
126 from rhodecode.lib.base import attach_context_attributes
84 from rhodecode.lib.base import attach_context_attributes
127 from rhodecode.model.notification import EmailNotificationModel
85 from rhodecode.model.notification import EmailNotificationModel
128
86
129 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
87 recipients = aslist(app.CONFIG.get("exception_tracker.send_email_recipients", ""))
130 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
88 log.debug("Sending Email exception to: `%s`", recipients or "all super admins")
131
89
132 # NOTE(marcink): needed for email template rendering
90 # NOTE(marcink): needed for email template rendering
133 user_id = None
91 user_id = None
134 if hasattr(request, 'user'):
92 if hasattr(request, "user"):
135 user_id = request.user.user_id
93 user_id = request.user.user_id
136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
94 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
137
95
138 email_kwargs = {
96 email_kwargs = {
139 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
97 "email_prefix": app.CONFIG.get("exception_tracker.email_prefix", "")
140 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
98 or "[RHODECODE ERROR]",
141 'exc_id': exc_id,
99 "exc_url": request.route_url(
142 'exc_type_name': exc_type_name,
100 "admin_settings_exception_tracker_show", exception_id=exc_id
143 'exc_traceback': read_exception(exc_id, prefix=None),
101 ),
102 "exc_id": exc_id,
103 "exc_type_name": exc_type_name,
104 "exc_traceback": read_exception(exc_id, prefix=None),
144 }
105 }
145
106
146 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
107 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
147 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
108 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs
109 )
110
111 run_task(tasks.send_email, recipients, subject, email_body_plaintext, email_body)
112
113
114 def get_exc_store():
115 """
116 Get and create exception store if it's not existing
117 """
118 global _exc_store
148
119
149 run_task(tasks.send_email, recipients, subject,
120 if _exc_store is not None:
150 email_body_plaintext, email_body)
121 # quick global cache
122 return _exc_store
123
124 import rhodecode as app
125
126 exc_store_dir = (
127 app.CONFIG.get("exception_tracker.store_path", "") or tempfile.gettempdir()
128 )
129 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
130
131 _exc_store_path = os.path.abspath(_exc_store_path)
132 if not os.path.isdir(_exc_store_path):
133 os.makedirs(_exc_store_path)
134 log.debug("Initializing exceptions store at %s", _exc_store_path)
135 _exc_store = _exc_store_path
136
137 return _exc_store_path
151
138
152
139
153 def _prepare_exception(exc_info):
140 def get_detailed_tb(exc_info):
154 exc_type, exc_value, exc_traceback = exc_info
141 try:
155 exc_type_name = exc_type.__name__
142 from pip._vendor.rich import (
143 traceback as rich_tb,
144 scope as rich_scope,
145 console as rich_console,
146 )
147 except ImportError:
148 try:
149 from rich import (
150 traceback as rich_tb,
151 scope as rich_scope,
152 console as rich_console,
153 )
154 except ImportError:
155 return None
156
157 console = rich_console.Console(width=160, file=io.StringIO())
158
159 exc = rich_tb.Traceback.extract(*exc_info, show_locals=True)
156
160
157 tb = ''.join(traceback.format_exception(
161 tb_rich = rich_tb.Traceback(
158 exc_type, exc_value, exc_traceback, None))
162 trace=exc,
163 width=160,
164 extra_lines=3,
165 theme=None,
166 word_wrap=False,
167 show_locals=False,
168 max_frames=100,
169 )
159
170
160 return exc_type_name, tb
171 # last_stack = exc.stacks[-1]
172 # last_frame = last_stack.frames[-1]
173 # if last_frame and last_frame.locals:
174 # console.print(
175 # rich_scope.render_scope(
176 # last_frame.locals,
177 # title=f'{last_frame.filename}:{last_frame.lineno}'))
178
179 console.print(tb_rich)
180 formatted_locals = console.file.getvalue()
181
182 return formatted_locals
161
183
162
184
163 def store_exception(exc_id, exc_info, prefix=global_prefix, send_email=None):
185 def get_request_metadata(request=None) -> dict:
186 request_metadata = {}
187 if not request:
188 from pyramid.threadlocal import get_current_request
189
190 request = get_current_request()
191
192 # NOTE(marcink): store request information into exc_data
193 if request:
194 request_metadata["client_address"] = getattr(request, "client_addr", "")
195 request_metadata["user_agent"] = getattr(request, "user_agent", "")
196 request_metadata["method"] = getattr(request, "method", "")
197 request_metadata["url"] = getattr(request, "url", "")
198 return request_metadata
199
200
201 def format_exc(exc_info):
202 exc_type, exc_value, exc_traceback = exc_info
203 tb = "++ TRACEBACK ++\n\n"
204 tb += "".join(traceback.format_exception(exc_type, exc_value, exc_traceback, None))
205
206 locals_tb = get_detailed_tb(exc_info)
207 if locals_tb:
208 tb += f"\n+++ DETAILS +++\n\n{locals_tb}\n" ""
209 return tb
210
211
212 def _store_exception(exc_id, exc_info, prefix, request_path='', send_email=None):
213 """
214 Low level function to store exception in the exception tracker
215 """
216
217 extra_data = {}
218 extra_data.update(get_request_metadata())
219
220 exc_type, exc_value, exc_traceback = exc_info
221 tb = format_exc(exc_info)
222
223 exc_type_name = exc_type.__name__
224 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name, extra_data=extra_data)
225
226 exc_pref_id = f"{exc_id}_{prefix}_{org_data['exc_timestamp']}"
227 exc_store_path = get_exc_store()
228 if not os.path.isdir(exc_store_path):
229 os.makedirs(exc_store_path)
230 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
231 with open(stored_exc_path, "wb") as f:
232 f.write(exc_data)
233 log.debug("Stored generated exception %s as: %s", exc_id, stored_exc_path)
234
235 if request_path:
236 log.error(
237 'error occurred handling this request.\n'
238 'Path: `%s`, %s',
239 request_path, tb)
240
241 maybe_send_exc_email(exc_id, exc_type_name, send_email)
242
243
244 def store_exception(exc_id, exc_info, prefix=global_prefix, request_path='', send_email=None):
164 """
245 """
165 Example usage::
246 Example usage::
166
247
@@ -169,12 +250,16 b' def store_exception(exc_id, exc_info, pr'
169 """
250 """
170
251
171 try:
252 try:
172 exc_type_name, exc_traceback = _prepare_exception(exc_info)
253 exc_type = exc_info[0]
173 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
254 exc_type_name = exc_type.__name__
174 exc_traceback=exc_traceback, prefix=prefix, send_email=send_email)
255
256 _store_exception(
257 exc_id=exc_id, exc_info=exc_info, prefix=prefix, request_path=request_path,
258 send_email=send_email
259 )
175 return exc_id, exc_type_name
260 return exc_id, exc_type_name
176 except Exception:
261 except Exception:
177 log.exception('Failed to store exception `%s` information', exc_id)
262 log.exception("Failed to store exception `%s` information", exc_id)
178 # there's no way this can fail, it will crash server badly if it does.
263 # there's no way this can fail, it will crash server badly if it does.
179 pass
264 pass
180
265
@@ -182,13 +267,13 b' def store_exception(exc_id, exc_info, pr'
182 def _find_exc_file(exc_id, prefix=global_prefix):
267 def _find_exc_file(exc_id, prefix=global_prefix):
183 exc_store_path = get_exc_store()
268 exc_store_path = get_exc_store()
184 if prefix:
269 if prefix:
185 exc_id = f'{exc_id}_{prefix}'
270 exc_id = f"{exc_id}_{prefix}"
186 else:
271 else:
187 # search without a prefix
272 # search without a prefix
188 exc_id = f'{exc_id}'
273 exc_id = f"{exc_id}"
189
274
190 found_exc_id = None
275 found_exc_id = None
191 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
276 matches = glob.glob(os.path.join(exc_store_path, exc_id) + "*")
192 if matches:
277 if matches:
193 found_exc_id = matches[0]
278 found_exc_id = matches[0]
194
279
@@ -198,10 +283,10 b' def _find_exc_file(exc_id, prefix=global'
198 def _read_exception(exc_id, prefix):
283 def _read_exception(exc_id, prefix):
199 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
284 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
200 if exc_id_file_path:
285 if exc_id_file_path:
201 with open(exc_id_file_path, 'rb') as f:
286 with open(exc_id_file_path, "rb") as f:
202 return exc_unserialize(f.read())
287 return exc_unserialize(f.read())
203 else:
288 else:
204 log.debug('Exception File `%s` not found', exc_id_file_path)
289 log.debug("Exception File `%s` not found", exc_id_file_path)
205 return None
290 return None
206
291
207
292
@@ -209,7 +294,7 b' def read_exception(exc_id, prefix=global'
209 try:
294 try:
210 return _read_exception(exc_id=exc_id, prefix=prefix)
295 return _read_exception(exc_id=exc_id, prefix=prefix)
211 except Exception:
296 except Exception:
212 log.exception('Failed to read exception `%s` information', exc_id)
297 log.exception("Failed to read exception `%s` information", exc_id)
213 # there's no way this can fail, it will crash server badly if it does.
298 # there's no way this can fail, it will crash server badly if it does.
214 return None
299 return None
215
300
@@ -221,7 +306,7 b' def delete_exception(exc_id, prefix=glob'
221 os.remove(exc_id_file_path)
306 os.remove(exc_id_file_path)
222
307
223 except Exception:
308 except Exception:
224 log.exception('Failed to remove exception `%s` information', exc_id)
309 log.exception("Failed to remove exception `%s` information", exc_id)
225 # there's no way this can fail, it will crash server badly if it does.
310 # there's no way this can fail, it will crash server badly if it does.
226 pass
311 pass
227
312
General Comments 0
You need to be logged in to leave comments. Login now