##// END OF EJS Templates
exceptions: store exception about failed exc email sending
super-admin -
r4811:201c0563 default
parent child Browse files
Show More
@@ -1,230 +1,232 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import time
22 import time
23 import sys
23 import datetime
24 import datetime
24 import msgpack
25 import msgpack
25 import logging
26 import logging
26 import traceback
27 import traceback
27 import tempfile
28 import tempfile
28 import glob
29 import glob
29
30
30 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
31
32
32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
33 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
33 global_prefix = 'rhodecode'
34 global_prefix = 'rhodecode'
34 exc_store_dir_name = 'rc_exception_store_v1'
35 exc_store_dir_name = 'rc_exception_store_v1'
35
36
36
37
37 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
38 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
38
39
39 data = {
40 data = {
40 'version': 'v1',
41 'version': 'v1',
41 'exc_id': exc_id,
42 'exc_id': exc_id,
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 'exc_timestamp': repr(time.time()),
44 'exc_timestamp': repr(time.time()),
44 'exc_message': tb,
45 'exc_message': tb,
45 'exc_type': exc_type,
46 'exc_type': exc_type,
46 }
47 }
47 if extra_data:
48 if extra_data:
48 data.update(extra_data)
49 data.update(extra_data)
49 return msgpack.packb(data), data
50 return msgpack.packb(data), data
50
51
51
52
52 def exc_unserialize(tb):
53 def exc_unserialize(tb):
53 return msgpack.unpackb(tb)
54 return msgpack.unpackb(tb)
54
55
55 _exc_store = None
56 _exc_store = None
56
57
57
58
58 def get_exc_store():
59 def get_exc_store():
59 """
60 """
60 Get and create exception store if it's not existing
61 Get and create exception store if it's not existing
61 """
62 """
62 global _exc_store
63 global _exc_store
63 import rhodecode as app
64 import rhodecode as app
64
65
65 if _exc_store is not None:
66 if _exc_store is not None:
66 # quick global cache
67 # quick global cache
67 return _exc_store
68 return _exc_store
68
69
69 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
70 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
70 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
71 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
71
72
72 _exc_store_path = os.path.abspath(_exc_store_path)
73 _exc_store_path = os.path.abspath(_exc_store_path)
73 if not os.path.isdir(_exc_store_path):
74 if not os.path.isdir(_exc_store_path):
74 os.makedirs(_exc_store_path)
75 os.makedirs(_exc_store_path)
75 log.debug('Initializing exceptions store at %s', _exc_store_path)
76 log.debug('Initializing exceptions store at %s', _exc_store_path)
76 _exc_store = _exc_store_path
77 _exc_store = _exc_store_path
77
78
78 return _exc_store_path
79 return _exc_store_path
79
80
80
81
81 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
82 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
82 """
83 """
83 Low level function to store exception in the exception tracker
84 Low level function to store exception in the exception tracker
84 """
85 """
85 from pyramid.threadlocal import get_current_request
86 from pyramid.threadlocal import get_current_request
86 import rhodecode as app
87 import rhodecode as app
87 request = get_current_request()
88 request = get_current_request()
88 extra_data = {}
89 extra_data = {}
89 # NOTE(marcink): store request information into exc_data
90 # NOTE(marcink): store request information into exc_data
90 if request:
91 if request:
91 extra_data['client_address'] = getattr(request, 'client_addr', '')
92 extra_data['client_address'] = getattr(request, 'client_addr', '')
92 extra_data['user_agent'] = getattr(request, 'user_agent', '')
93 extra_data['user_agent'] = getattr(request, 'user_agent', '')
93 extra_data['method'] = getattr(request, 'method', '')
94 extra_data['method'] = getattr(request, 'method', '')
94 extra_data['url'] = getattr(request, 'url', '')
95 extra_data['url'] = getattr(request, 'url', '')
95
96
96 exc_store_path = get_exc_store()
97 exc_store_path = get_exc_store()
97 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name, extra_data=extra_data)
98 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name, extra_data=extra_data)
98
99
99 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
100 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
100 if not os.path.isdir(exc_store_path):
101 if not os.path.isdir(exc_store_path):
101 os.makedirs(exc_store_path)
102 os.makedirs(exc_store_path)
102 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
103 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
103 with open(stored_exc_path, 'wb') as f:
104 with open(stored_exc_path, 'wb') as f:
104 f.write(exc_data)
105 f.write(exc_data)
105 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
106 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
106
107
107 if send_email is None:
108 if send_email is None:
108 # NOTE(marcink): read app config unless we specify explicitly
109 # NOTE(marcink): read app config unless we specify explicitly
109 send_email = app.CONFIG.get('exception_tracker.send_email', False)
110 send_email = app.CONFIG.get('exception_tracker.send_email', False)
110
111
111 mail_server = app.CONFIG.get('smtp_server') or None
112 mail_server = app.CONFIG.get('smtp_server') or None
112 send_email = send_email and mail_server
113 send_email = send_email and mail_server
113 if send_email and request:
114 if send_email and request:
114 try:
115 try:
115 send_exc_email(request, exc_id, exc_type_name)
116 send_exc_email(request, exc_id, exc_type_name)
116 except Exception:
117 except Exception:
117 log.exception('Failed to send exception email')
118 log.exception('Failed to send exception email')
118 pass
119 exc_info = sys.exc_info()
120 store_exception(id(exc_info), exc_info, send_email=False)
119
121
120
122
121 def send_exc_email(request, exc_id, exc_type_name):
123 def send_exc_email(request, exc_id, exc_type_name):
122 import rhodecode as app
124 import rhodecode as app
123 from rhodecode.apps._base import TemplateArgs
125 from rhodecode.apps._base import TemplateArgs
124 from rhodecode.lib.utils2 import aslist
126 from rhodecode.lib.utils2 import aslist
125 from rhodecode.lib.celerylib import run_task, tasks
127 from rhodecode.lib.celerylib import run_task, tasks
126 from rhodecode.lib.base import attach_context_attributes
128 from rhodecode.lib.base import attach_context_attributes
127 from rhodecode.model.notification import EmailNotificationModel
129 from rhodecode.model.notification import EmailNotificationModel
128
130
129 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
131 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
130 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
132 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
131
133
132 # NOTE(marcink): needed for email template rendering
134 # NOTE(marcink): needed for email template rendering
133 user_id = None
135 user_id = None
134 if hasattr(request, 'user'):
136 if hasattr(request, 'user'):
135 user_id = request.user.user_id
137 user_id = request.user.user_id
136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
138 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
137
139
138 email_kwargs = {
140 email_kwargs = {
139 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
141 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
140 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
142 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
141 'exc_id': exc_id,
143 'exc_id': exc_id,
142 'exc_type_name': exc_type_name,
144 'exc_type_name': exc_type_name,
143 'exc_traceback': read_exception(exc_id, prefix=None),
145 'exc_traceback': read_exception(exc_id, prefix=None),
144 }
146 }
145
147
146 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
148 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
147 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
149 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
148
150
149 run_task(tasks.send_email, recipients, subject,
151 run_task(tasks.send_email, recipients, subject,
150 email_body_plaintext, email_body)
152 email_body_plaintext, email_body)
151
153
152
154
153 def _prepare_exception(exc_info):
155 def _prepare_exception(exc_info):
154 exc_type, exc_value, exc_traceback = exc_info
156 exc_type, exc_value, exc_traceback = exc_info
155 exc_type_name = exc_type.__name__
157 exc_type_name = exc_type.__name__
156
158
157 tb = ''.join(traceback.format_exception(
159 tb = ''.join(traceback.format_exception(
158 exc_type, exc_value, exc_traceback, None))
160 exc_type, exc_value, exc_traceback, None))
159
161
160 return exc_type_name, tb
162 return exc_type_name, tb
161
163
162
164
163 def store_exception(exc_id, exc_info, prefix=global_prefix):
165 def store_exception(exc_id, exc_info, prefix=global_prefix, send_email=None):
164 """
166 """
165 Example usage::
167 Example usage::
166
168
167 exc_info = sys.exc_info()
169 exc_info = sys.exc_info()
168 store_exception(id(exc_info), exc_info)
170 store_exception(id(exc_info), exc_info)
169 """
171 """
170
172
171 try:
173 try:
172 exc_type_name, exc_traceback = _prepare_exception(exc_info)
174 exc_type_name, exc_traceback = _prepare_exception(exc_info)
173 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
175 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
174 exc_traceback=exc_traceback, prefix=prefix)
176 exc_traceback=exc_traceback, prefix=prefix, send_email=send_email)
175 return exc_id, exc_type_name
177 return exc_id, exc_type_name
176 except Exception:
178 except Exception:
177 log.exception('Failed to store exception `%s` information', exc_id)
179 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.
180 # there's no way this can fail, it will crash server badly if it does.
179 pass
181 pass
180
182
181
183
182 def _find_exc_file(exc_id, prefix=global_prefix):
184 def _find_exc_file(exc_id, prefix=global_prefix):
183 exc_store_path = get_exc_store()
185 exc_store_path = get_exc_store()
184 if prefix:
186 if prefix:
185 exc_id = '{}_{}'.format(exc_id, prefix)
187 exc_id = '{}_{}'.format(exc_id, prefix)
186 else:
188 else:
187 # search without a prefix
189 # search without a prefix
188 exc_id = '{}'.format(exc_id)
190 exc_id = '{}'.format(exc_id)
189
191
190 found_exc_id = None
192 found_exc_id = None
191 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
193 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
192 if matches:
194 if matches:
193 found_exc_id = matches[0]
195 found_exc_id = matches[0]
194
196
195 return found_exc_id
197 return found_exc_id
196
198
197
199
198 def _read_exception(exc_id, prefix):
200 def _read_exception(exc_id, prefix):
199 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
201 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
200 if exc_id_file_path:
202 if exc_id_file_path:
201 with open(exc_id_file_path, 'rb') as f:
203 with open(exc_id_file_path, 'rb') as f:
202 return exc_unserialize(f.read())
204 return exc_unserialize(f.read())
203 else:
205 else:
204 log.debug('Exception File `%s` not found', exc_id_file_path)
206 log.debug('Exception File `%s` not found', exc_id_file_path)
205 return None
207 return None
206
208
207
209
208 def read_exception(exc_id, prefix=global_prefix):
210 def read_exception(exc_id, prefix=global_prefix):
209 try:
211 try:
210 return _read_exception(exc_id=exc_id, prefix=prefix)
212 return _read_exception(exc_id=exc_id, prefix=prefix)
211 except Exception:
213 except Exception:
212 log.exception('Failed to read exception `%s` information', exc_id)
214 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.
215 # there's no way this can fail, it will crash server badly if it does.
214 return None
216 return None
215
217
216
218
217 def delete_exception(exc_id, prefix=global_prefix):
219 def delete_exception(exc_id, prefix=global_prefix):
218 try:
220 try:
219 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
221 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
220 if exc_id_file_path:
222 if exc_id_file_path:
221 os.remove(exc_id_file_path)
223 os.remove(exc_id_file_path)
222
224
223 except Exception:
225 except Exception:
224 log.exception('Failed to remove exception `%s` information', exc_id)
226 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.
227 # there's no way this can fail, it will crash server badly if it does.
226 pass
228 pass
227
229
228
230
229 def generate_id():
231 def generate_id():
230 return id(object())
232 return id(object())
General Comments 0
You need to be logged in to leave comments. Login now