##// END OF EJS Templates
events: add logging for all events triggered
dan -
r428:8b8343c0 default
parent child Browse files
Show More
@@ -1,66 +1,70 b''
1 # Copyright (C) 2016-2016 RhodeCode GmbH
1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
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 logging
19 from pyramid.threadlocal import get_current_registry
20 from pyramid.threadlocal import get_current_registry
20
21
22 log = logging.getLogger()
23
21
24
22 def trigger(event, registry=None):
25 def trigger(event, registry=None):
23 """
26 """
24 Helper method to send an event. This wraps the pyramid logic to send an
27 Helper method to send an event. This wraps the pyramid logic to send an
25 event.
28 event.
26 """
29 """
27 # For the first step we are using pyramids thread locals here. If the
30 # For the first step we are using pyramids thread locals here. If the
28 # event mechanism works out as a good solution we should think about
31 # event mechanism works out as a good solution we should think about
29 # passing the registry as an argument to get rid of it.
32 # passing the registry as an argument to get rid of it.
30 registry = registry or get_current_registry()
33 registry = registry or get_current_registry()
31 registry.notify(event)
34 registry.notify(event)
35 log.debug('event %s triggered', event)
32
36
33 # Until we can work around the problem that VCS operations do not have a
37 # Until we can work around the problem that VCS operations do not have a
34 # pyramid context to work with, we send the events to integrations directly
38 # pyramid context to work with, we send the events to integrations directly
35
39
36 # Later it will be possible to use regular pyramid subscribers ie:
40 # Later it will be possible to use regular pyramid subscribers ie:
37 # config.add_subscriber(integrations_event_handler, RhodecodeEvent)
41 # config.add_subscriber(integrations_event_handler, RhodecodeEvent)
38 from rhodecode.integrations import integrations_event_handler
42 from rhodecode.integrations import integrations_event_handler
39 if isinstance(event, RhodecodeEvent):
43 if isinstance(event, RhodecodeEvent):
40 integrations_event_handler(event)
44 integrations_event_handler(event)
41
45
42
46
43 from rhodecode.events.base import RhodecodeEvent
47 from rhodecode.events.base import RhodecodeEvent
44
48
45 from rhodecode.events.user import (
49 from rhodecode.events.user import (
46 UserPreCreate,
50 UserPreCreate,
47 UserPreUpdate,
51 UserPreUpdate,
48 UserRegistered
52 UserRegistered
49 )
53 )
50
54
51 from rhodecode.events.repo import (
55 from rhodecode.events.repo import (
52 RepoEvent,
56 RepoEvent,
53 RepoPreCreateEvent, RepoCreateEvent,
57 RepoPreCreateEvent, RepoCreateEvent,
54 RepoPreDeleteEvent, RepoDeleteEvent,
58 RepoPreDeleteEvent, RepoDeleteEvent,
55 RepoPrePushEvent, RepoPushEvent,
59 RepoPrePushEvent, RepoPushEvent,
56 RepoPrePullEvent, RepoPullEvent,
60 RepoPrePullEvent, RepoPullEvent,
57 )
61 )
58
62
59 from rhodecode.events.pullrequest import (
63 from rhodecode.events.pullrequest import (
60 PullRequestEvent,
64 PullRequestEvent,
61 PullRequestCreateEvent,
65 PullRequestCreateEvent,
62 PullRequestUpdateEvent,
66 PullRequestUpdateEvent,
63 PullRequestReviewEvent,
67 PullRequestReviewEvent,
64 PullRequestMergeEvent,
68 PullRequestMergeEvent,
65 PullRequestCloseEvent,
69 PullRequestCloseEvent,
66 )
70 )
@@ -1,202 +1,201 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22
22
23 import re
23 import re
24 import logging
24 import logging
25 import requests
25 import requests
26 import colander
26 import colander
27 from celery.task import task
27 from celery.task import task
28 from mako.template import Template
28 from mako.template import Template
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
31 from rhodecode.translation import lazy_ugettext
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib.celerylib import run_task
33 from rhodecode.lib.celerylib import run_task
34 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.integrations.types.base import IntegrationTypeBase
35 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
36 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
37
37
38 log = logging.getLogger()
38 log = logging.getLogger()
39
39
40
40
41 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
41 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
42 service = colander.SchemaNode(
42 service = colander.SchemaNode(
43 colander.String(),
43 colander.String(),
44 title=lazy_ugettext('Slack service URL'),
44 title=lazy_ugettext('Slack service URL'),
45 description=h.literal(lazy_ugettext(
45 description=h.literal(lazy_ugettext(
46 'This can be setup at the '
46 'This can be setup at the '
47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
48 'slack app manager</a>')),
48 'slack app manager</a>')),
49 default='',
49 default='',
50 placeholder='https://hooks.slack.com/services/...',
50 placeholder='https://hooks.slack.com/services/...',
51 preparer=strip_whitespace,
51 preparer=strip_whitespace,
52 validator=colander.url,
52 validator=colander.url,
53 widget='string'
53 widget='string'
54 )
54 )
55 username = colander.SchemaNode(
55 username = colander.SchemaNode(
56 colander.String(),
56 colander.String(),
57 title=lazy_ugettext('Username'),
57 title=lazy_ugettext('Username'),
58 description=lazy_ugettext('Username to show notifications coming from.'),
58 description=lazy_ugettext('Username to show notifications coming from.'),
59 missing='Rhodecode',
59 missing='Rhodecode',
60 preparer=strip_whitespace,
60 preparer=strip_whitespace,
61 widget='string',
61 widget='string',
62 placeholder='Rhodecode'
62 placeholder='Rhodecode'
63 )
63 )
64 channel = colander.SchemaNode(
64 channel = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 title=lazy_ugettext('Channel'),
66 title=lazy_ugettext('Channel'),
67 description=lazy_ugettext('Channel to send notifications to.'),
67 description=lazy_ugettext('Channel to send notifications to.'),
68 missing='',
68 missing='',
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 widget='string',
70 widget='string',
71 placeholder='#general'
71 placeholder='#general'
72 )
72 )
73 icon_emoji = colander.SchemaNode(
73 icon_emoji = colander.SchemaNode(
74 colander.String(),
74 colander.String(),
75 title=lazy_ugettext('Emoji'),
75 title=lazy_ugettext('Emoji'),
76 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
76 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
77 missing='',
77 missing='',
78 preparer=strip_whitespace,
78 preparer=strip_whitespace,
79 widget='string',
79 widget='string',
80 placeholder=':studio_microphone:'
80 placeholder=':studio_microphone:'
81 )
81 )
82
82
83
83
84 repo_push_template = Template(r'''
84 repo_push_template = Template(r'''
85 *${data['actor']['username']}* pushed to \
85 *${data['actor']['username']}* pushed to \
86 %if data['push']['branches']:
86 %if data['push']['branches']:
87 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
87 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
88 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
88 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
89 %else:
89 %else:
90 unknown branch \
90 unknown branch \
91 %endif
91 %endif
92 in <${data['repo']['url']}|${data['repo']['repo_name']}>
92 in <${data['repo']['url']}|${data['repo']['repo_name']}>
93 >>>
93 >>>
94 %for commit in data['push']['commits']:
94 %for commit in data['push']['commits']:
95 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
95 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
96 %endfor
96 %endfor
97 ''')
97 ''')
98
98
99
99
100 class SlackIntegrationType(IntegrationTypeBase):
100 class SlackIntegrationType(IntegrationTypeBase):
101 key = 'slack'
101 key = 'slack'
102 display_name = lazy_ugettext('Slack')
102 display_name = lazy_ugettext('Slack')
103 SettingsSchema = SlackSettingsSchema
103 SettingsSchema = SlackSettingsSchema
104 valid_events = [
104 valid_events = [
105 events.PullRequestCloseEvent,
105 events.PullRequestCloseEvent,
106 events.PullRequestMergeEvent,
106 events.PullRequestMergeEvent,
107 events.PullRequestUpdateEvent,
107 events.PullRequestUpdateEvent,
108 events.PullRequestReviewEvent,
108 events.PullRequestReviewEvent,
109 events.PullRequestCreateEvent,
109 events.PullRequestCreateEvent,
110 events.RepoPushEvent,
110 events.RepoPushEvent,
111 events.RepoCreateEvent,
111 events.RepoCreateEvent,
112 ]
112 ]
113
113
114 def send_event(self, event):
114 def send_event(self, event):
115 if event.__class__ not in self.valid_events:
115 if event.__class__ not in self.valid_events:
116 log.debug('event not valid: %r' % event)
116 log.debug('event not valid: %r' % event)
117 return
117 return
118
118
119 if event.name not in self.settings['events']:
119 if event.name not in self.settings['events']:
120 log.debug('event ignored: %r' % event)
120 log.debug('event ignored: %r' % event)
121 return
121 return
122
122
123 data = event.as_dict()
123 data = event.as_dict()
124
124
125 text = '*%s* caused a *%s* event' % (
125 text = '*%s* caused a *%s* event' % (
126 data['actor']['username'], event.name)
126 data['actor']['username'], event.name)
127
127
128 log.debug('handling slack event for %s' % event.name)
128 log.debug('handling slack event for %s' % event.name)
129
129
130 if isinstance(event, events.PullRequestEvent):
130 if isinstance(event, events.PullRequestEvent):
131 text = self.format_pull_request_event(event, data)
131 text = self.format_pull_request_event(event, data)
132 elif isinstance(event, events.RepoPushEvent):
132 elif isinstance(event, events.RepoPushEvent):
133 text = self.format_repo_push_event(data)
133 text = self.format_repo_push_event(data)
134 elif isinstance(event, events.RepoCreateEvent):
134 elif isinstance(event, events.RepoCreateEvent):
135 text = self.format_repo_create_event(data)
135 text = self.format_repo_create_event(data)
136 else:
136 else:
137 log.error('unhandled event type: %r' % event)
137 log.error('unhandled event type: %r' % event)
138
138
139 run_task(post_text_to_slack, self.settings, text)
139 run_task(post_text_to_slack, self.settings, text)
140
140
141 @classmethod
141 @classmethod
142 def settings_schema(cls):
142 def settings_schema(cls):
143 schema = SlackSettingsSchema()
143 schema = SlackSettingsSchema()
144 schema.add(colander.SchemaNode(
144 schema.add(colander.SchemaNode(
145 colander.Set(),
145 colander.Set(),
146 widget='checkbox_list',
146 widget='checkbox_list',
147 choices=sorted([e.name for e in cls.valid_events]),
147 choices=sorted([e.name for e in cls.valid_events]),
148 description="Events activated for this integration",
148 description="Events activated for this integration",
149 # default=[e.name for e in cls.valid_events],
150 name='events'
149 name='events'
151 ))
150 ))
152 return schema
151 return schema
153
152
154 def format_pull_request_event(self, event, data):
153 def format_pull_request_event(self, event, data):
155 action = {
154 action = {
156 events.PullRequestCloseEvent: 'closed',
155 events.PullRequestCloseEvent: 'closed',
157 events.PullRequestMergeEvent: 'merged',
156 events.PullRequestMergeEvent: 'merged',
158 events.PullRequestUpdateEvent: 'updated',
157 events.PullRequestUpdateEvent: 'updated',
159 events.PullRequestReviewEvent: 'reviewed',
158 events.PullRequestReviewEvent: 'reviewed',
160 events.PullRequestCreateEvent: 'created',
159 events.PullRequestCreateEvent: 'created',
161 }.get(event.__class__, '<unknown action>')
160 }.get(event.__class__, '<unknown action>')
162
161
163 return ('Pull request <{url}|#{number}> ({title}) '
162 return ('Pull request <{url}|#{number}> ({title}) '
164 '{action} by {user}').format(
163 '{action} by {user}').format(
165 user=data['actor']['username'],
164 user=data['actor']['username'],
166 number=data['pullrequest']['pull_request_id'],
165 number=data['pullrequest']['pull_request_id'],
167 url=data['pullrequest']['url'],
166 url=data['pullrequest']['url'],
168 title=data['pullrequest']['title'],
167 title=data['pullrequest']['title'],
169 action=action
168 action=action
170 )
169 )
171
170
172 def format_repo_push_event(self, data):
171 def format_repo_push_event(self, data):
173 result = repo_push_template.render(
172 result = repo_push_template.render(
174 data=data,
173 data=data,
175 html_to_slack_links=html_to_slack_links,
174 html_to_slack_links=html_to_slack_links,
176 )
175 )
177 return result
176 return result
178
177
179 def format_repo_create_event(self, data):
178 def format_repo_create_event(self, data):
180 return '<{}|{}> ({}) repository created by *{}*'.format(
179 return '<{}|{}> ({}) repository created by *{}*'.format(
181 data['repo']['url'],
180 data['repo']['url'],
182 data['repo']['repo_name'],
181 data['repo']['repo_name'],
183 data['repo']['repo_type'],
182 data['repo']['repo_type'],
184 data['actor']['username'],
183 data['actor']['username'],
185 )
184 )
186
185
187
186
188 def html_to_slack_links(message):
187 def html_to_slack_links(message):
189 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
188 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
190 r'<\1|\2>', message)
189 r'<\1|\2>', message)
191
190
192
191
193 @task(ignore_result=True)
192 @task(ignore_result=True)
194 def post_text_to_slack(settings, text):
193 def post_text_to_slack(settings, text):
195 log.debug('sending %s to slack %s' % (text, settings['service']))
194 log.debug('sending %s to slack %s' % (text, settings['service']))
196 resp = requests.post(settings['service'], json={
195 resp = requests.post(settings['service'], json={
197 "channel": settings.get('channel', ''),
196 "channel": settings.get('channel', ''),
198 "username": settings.get('username', 'Rhodecode'),
197 "username": settings.get('username', 'Rhodecode'),
199 "text": text,
198 "text": text,
200 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
199 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
201 })
200 })
202 resp.raise_for_status() # raise exception on a failed request
201 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now