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