##// END OF EJS Templates
exception-tracker: store event sending exception for easier event fail debugging.
marcink -
r3007:a41e7b95 default
parent child Browse files
Show More
@@ -1,70 +1,74 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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 import sys
21 import logging
21 import logging
22
22
23 from rhodecode.integrations.registry import IntegrationTypeRegistry
23 from rhodecode.integrations.registry import IntegrationTypeRegistry
24 from rhodecode.integrations.types import webhook, slack, hipchat, email, base
24 from rhodecode.integrations.types import webhook, slack, hipchat, email, base
25 from rhodecode.lib.exc_tracking import store_exception
26
25 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
26
28
27
29
28 # TODO: dan: This is currently global until we figure out what to do about
30 # TODO: dan: This is currently global until we figure out what to do about
29 # VCS's not having a pyramid context - move it to pyramid app configuration
31 # VCS's not having a pyramid context - move it to pyramid app configuration
30 # includeme level later to allow per instance integration setup
32 # includeme level later to allow per instance integration setup
31 integration_type_registry = IntegrationTypeRegistry()
33 integration_type_registry = IntegrationTypeRegistry()
32
34
33 integration_type_registry.register_integration_type(
35 integration_type_registry.register_integration_type(
34 webhook.WebhookIntegrationType)
36 webhook.WebhookIntegrationType)
35 integration_type_registry.register_integration_type(
37 integration_type_registry.register_integration_type(
36 slack.SlackIntegrationType)
38 slack.SlackIntegrationType)
37 integration_type_registry.register_integration_type(
39 integration_type_registry.register_integration_type(
38 hipchat.HipchatIntegrationType)
40 hipchat.HipchatIntegrationType)
39 integration_type_registry.register_integration_type(
41 integration_type_registry.register_integration_type(
40 email.EmailIntegrationType)
42 email.EmailIntegrationType)
41
43
42
44
43 # dummy EE integration to show users what we have in EE edition
45 # dummy EE integration to show users what we have in EE edition
44 integration_type_registry.register_integration_type(
46 integration_type_registry.register_integration_type(
45 base.EEIntegration('Jira Issues integration', 'jira'))
47 base.EEIntegration('Jira Issues integration', 'jira'))
46 integration_type_registry.register_integration_type(
48 integration_type_registry.register_integration_type(
47 base.EEIntegration('Redmine Tracker integration', 'redmine'))
49 base.EEIntegration('Redmine Tracker integration', 'redmine'))
48 integration_type_registry.register_integration_type(
50 integration_type_registry.register_integration_type(
49 base.EEIntegration('Jenkins CI integration', 'jenkins'))
51 base.EEIntegration('Jenkins CI integration', 'jenkins'))
50
52
51
53
52 def integrations_event_handler(event):
54 def integrations_event_handler(event):
53 """
55 """
54 Takes an event and passes it to all enabled integrations
56 Takes an event and passes it to all enabled integrations
55 """
57 """
56 from rhodecode.model.integration import IntegrationModel
58 from rhodecode.model.integration import IntegrationModel
57
59
58 integration_model = IntegrationModel()
60 integration_model = IntegrationModel()
59 integrations = integration_model.get_for_event(event)
61 integrations = integration_model.get_for_event(event)
60 for integration in integrations:
62 for integration in integrations:
61 try:
63 try:
62 integration_model.send_event(integration, event)
64 integration_model.send_event(integration, event)
63 except Exception:
65 except Exception:
66 exc_info = sys.exc_info()
67 store_exception(id(exc_info), exc_info)
64 log.exception(
68 log.exception(
65 'failure occurred when sending event %s to integration %s' % (
69 'failure occurred when sending event %s to integration %s' % (
66 event, integration))
70 event, integration))
67
71
68
72
69 def includeme(config):
73 def includeme(config):
70 config.include('rhodecode.integrations.routes')
74 config.include('rhodecode.integrations.routes')
@@ -1,146 +1,153 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 datetime
23 import datetime
24 import msgpack
24 import msgpack
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import tempfile
27 import tempfile
28
28
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32 # 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
33 global_prefix = 'rhodecode'
33 global_prefix = 'rhodecode'
34
34
35
35
36 def exc_serialize(exc_id, tb, exc_type):
36 def exc_serialize(exc_id, tb, exc_type):
37
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 return msgpack.packb(data), data
46 return msgpack.packb(data), data
47
47
48
48
49 def exc_unserialize(tb):
49 def exc_unserialize(tb):
50 return msgpack.unpackb(tb)
50 return msgpack.unpackb(tb)
51
51
52
52
53 def get_exc_store():
53 def get_exc_store():
54 """
54 """
55 Get and create exception store if it's not existing
55 Get and create exception store if it's not existing
56 """
56 """
57 exc_store_dir = 'rc_exception_store_v1'
57 exc_store_dir = 'rc_exception_store_v1'
58 # fallback
58 # fallback
59 _exc_store_path = os.path.join(tempfile.gettempdir(), exc_store_dir)
59 _exc_store_path = os.path.join(tempfile.gettempdir(), exc_store_dir)
60
60
61 exc_store_dir = '' # TODO: need a persistent cross instance store here
61 exc_store_dir = '' # TODO: need a persistent cross instance store here
62 if exc_store_dir:
62 if exc_store_dir:
63 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir)
63 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir)
64
64
65 _exc_store_path = os.path.abspath(_exc_store_path)
65 _exc_store_path = os.path.abspath(_exc_store_path)
66 if not os.path.isdir(_exc_store_path):
66 if not os.path.isdir(_exc_store_path):
67 os.makedirs(_exc_store_path)
67 os.makedirs(_exc_store_path)
68 log.debug('Initializing exceptions store at %s', _exc_store_path)
68 log.debug('Initializing exceptions store at %s', _exc_store_path)
69 return _exc_store_path
69 return _exc_store_path
70
70
71
71
72 def _store_exception(exc_id, exc_info, prefix):
72 def _store_exception(exc_id, exc_info, prefix):
73 exc_type, exc_value, exc_traceback = exc_info
73 exc_type, exc_value, exc_traceback = exc_info
74 tb = ''.join(traceback.format_exception(
74 tb = ''.join(traceback.format_exception(
75 exc_type, exc_value, exc_traceback, None))
75 exc_type, exc_value, exc_traceback, None))
76
76
77 exc_type_name = exc_type.__name__
77 exc_type_name = exc_type.__name__
78 exc_store_path = get_exc_store()
78 exc_store_path = get_exc_store()
79 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
79 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
80 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
80 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
81 if not os.path.isdir(exc_store_path):
81 if not os.path.isdir(exc_store_path):
82 os.makedirs(exc_store_path)
82 os.makedirs(exc_store_path)
83 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
83 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
84 with open(stored_exc_path, 'wb') as f:
84 with open(stored_exc_path, 'wb') as f:
85 f.write(exc_data)
85 f.write(exc_data)
86 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
86 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
87
87
88
88
89 def store_exception(exc_id, exc_info, prefix=global_prefix):
89 def store_exception(exc_id, exc_info, prefix=global_prefix):
90 """
91 Example usage::
92
93 exc_info = sys.exc_info()
94 store_exception(id(exc_info), exc_info)
95 """
96
90 try:
97 try:
91 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
98 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
92 except Exception:
99 except Exception:
93 log.exception('Failed to store exception `%s` information', exc_id)
100 log.exception('Failed to store exception `%s` information', exc_id)
94 # there's no way this can fail, it will crash server badly if it does.
101 # there's no way this can fail, it will crash server badly if it does.
95 pass
102 pass
96
103
97
104
98 def _find_exc_file(exc_id, prefix=global_prefix):
105 def _find_exc_file(exc_id, prefix=global_prefix):
99 exc_store_path = get_exc_store()
106 exc_store_path = get_exc_store()
100 if prefix:
107 if prefix:
101 exc_id = '{}_{}'.format(exc_id, prefix)
108 exc_id = '{}_{}'.format(exc_id, prefix)
102 else:
109 else:
103 # search without a prefix
110 # search without a prefix
104 exc_id = '{}'.format(exc_id)
111 exc_id = '{}'.format(exc_id)
105
112
106 # we need to search the store for such start pattern as above
113 # we need to search the store for such start pattern as above
107 for fname in os.listdir(exc_store_path):
114 for fname in os.listdir(exc_store_path):
108 if fname.startswith(exc_id):
115 if fname.startswith(exc_id):
109 exc_id = os.path.join(exc_store_path, fname)
116 exc_id = os.path.join(exc_store_path, fname)
110 break
117 break
111 continue
118 continue
112 else:
119 else:
113 exc_id = None
120 exc_id = None
114
121
115 return exc_id
122 return exc_id
116
123
117
124
118 def _read_exception(exc_id, prefix):
125 def _read_exception(exc_id, prefix):
119 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
126 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
120 if exc_id_file_path:
127 if exc_id_file_path:
121 with open(exc_id_file_path, 'rb') as f:
128 with open(exc_id_file_path, 'rb') as f:
122 return exc_unserialize(f.read())
129 return exc_unserialize(f.read())
123 else:
130 else:
124 log.debug('Exception File `%s` not found', exc_id_file_path)
131 log.debug('Exception File `%s` not found', exc_id_file_path)
125 return None
132 return None
126
133
127
134
128 def read_exception(exc_id, prefix=global_prefix):
135 def read_exception(exc_id, prefix=global_prefix):
129 try:
136 try:
130 return _read_exception(exc_id=exc_id, prefix=prefix)
137 return _read_exception(exc_id=exc_id, prefix=prefix)
131 except Exception:
138 except Exception:
132 log.exception('Failed to read exception `%s` information', exc_id)
139 log.exception('Failed to read exception `%s` information', exc_id)
133 # there's no way this can fail, it will crash server badly if it does.
140 # there's no way this can fail, it will crash server badly if it does.
134 return None
141 return None
135
142
136
143
137 def delete_exception(exc_id, prefix=global_prefix):
144 def delete_exception(exc_id, prefix=global_prefix):
138 try:
145 try:
139 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
146 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
140 if exc_id_file_path:
147 if exc_id_file_path:
141 os.remove(exc_id_file_path)
148 os.remove(exc_id_file_path)
142
149
143 except Exception:
150 except Exception:
144 log.exception('Failed to remove exception `%s` information', exc_id)
151 log.exception('Failed to remove exception `%s` information', exc_id)
145 # there's no way this can fail, it will crash server badly if it does.
152 # there's no way this can fail, it will crash server badly if it does.
146 pass
153 pass
General Comments 0
You need to be logged in to leave comments. Login now