##// END OF EJS Templates
Option to blacklist certain resources from AlertChannels
pagenoare -
Show More
@@ -0,0 +1,30 b''
1 """connect resources to alert_channels
2
3 Revision ID: e9fcfbdd9498
4 Revises: 55b6e612672f
5 Create Date: 2018-02-28 13:52:50.717217
6
7 """
8
9 # revision identifiers, used by Alembic.
10 revision = 'e9fcfbdd9498'
11 down_revision = '55b6e612672f'
12
13 from alembic import op
14 import sqlalchemy as sa
15
16
17 def upgrade():
18 op.create_table(
19 'channels_resources',
20 sa.Column('channel_pkey', sa.Integer,
21 sa.ForeignKey('alert_channels.pkey',
22 ondelete='CASCADE', onupdate='CASCADE')),
23 sa.Column('resource_id', sa.Integer,
24 sa.ForeignKey('resources.resource_id',
25 ondelete='CASCADE', onupdate='CASCADE'))
26 )
27
28
29 def downgrade():
30 op.drop_table('channels_resources')
@@ -1,291 +1,305 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 4 #
5 5 # Licensed under the Apache License, Version 2.0 (the "License");
6 6 # you may not use this file except in compliance with the License.
7 7 # You may obtain a copy of the License at
8 8 #
9 9 # http://www.apache.org/licenses/LICENSE-2.0
10 10 #
11 11 # Unless required by applicable law or agreed to in writing, software
12 12 # distributed under the License is distributed on an "AS IS" BASIS,
13 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 14 # See the License for the specific language governing permissions and
15 15 # limitations under the License.
16 16
17 17 import logging
18 18 import sqlalchemy as sa
19 19 import urllib.request, urllib.parse, urllib.error
20 20 from datetime import timedelta
21 21 from appenlight.models import Base
22 22 from appenlight.lib.utils.date_utils import convert_date
23 23 from sqlalchemy.dialects.postgresql import JSON
24 24 from ziggurat_foundations.models.base import BaseModel
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28 #
29 29 channel_rules_m2m_table = sa.Table(
30 30 'channels_actions', Base.metadata,
31 31 sa.Column('channel_pkey', sa.Integer,
32 32 sa.ForeignKey('alert_channels.pkey')),
33 33 sa.Column('action_pkey', sa.Integer,
34 34 sa.ForeignKey('alert_channels_actions.pkey'))
35 35 )
36 36
37 channel_resources_m2m_table = sa.Table(
38 'channels_resources', Base.metadata,
39 sa.Column('channel_pkey', sa.Integer,
40 sa.ForeignKey('alert_channels.pkey')),
41 sa.Column('resource_id', sa.Integer,
42 sa.ForeignKey('resources.resource_id'))
43 )
44
37 45 DATE_FRMT = '%Y-%m-%dT%H:%M'
38 46
39 47
40 48 class AlertChannel(Base, BaseModel):
41 49 """
42 50 Stores information about possible alerting options
43 51 """
44 52 __tablename__ = 'alert_channels'
45 53 __possible_channel_names__ = ['email']
46 54 __mapper_args__ = {
47 55 'polymorphic_on': 'channel_name',
48 56 'polymorphic_identity': 'integration'
49 57 }
50 58
51 59 owner_id = sa.Column(sa.Unicode(30),
52 60 sa.ForeignKey('users.id', onupdate='CASCADE',
53 61 ondelete='CASCADE'))
54 62 channel_name = sa.Column(sa.Unicode(25), nullable=False)
55 63 channel_value = sa.Column(sa.Unicode(80), nullable=False, default='')
56 64 channel_json_conf = sa.Column(JSON(), nullable=False, default='')
57 65 channel_validated = sa.Column(sa.Boolean, nullable=False,
58 66 default=False)
59 67 send_alerts = sa.Column(sa.Boolean, nullable=False,
60 68 default=True)
61 69 daily_digest = sa.Column(sa.Boolean, nullable=False,
62 70 default=True)
63 71 integration_id = sa.Column(sa.Integer, sa.ForeignKey('integrations.id'),
64 72 nullable=True)
65 73 pkey = sa.Column(sa.Integer(), nullable=False, primary_key=True)
66 74
67 75 channel_actions = sa.orm.relationship('AlertChannelAction',
68 76 cascade="all",
69 77 passive_deletes=True,
70 78 passive_updates=True,
71 79 secondary=channel_rules_m2m_table,
72 80 backref='channels')
81 resources = sa.orm.relationship('Resource',
82 cascade="all, delete-orphan",
83 passive_deletes=True,
84 passive_updates=True,
85 secondary=channel_resources_m2m_table,
86 backref='resources')
73 87
74 88 @property
75 89 def channel_visible_value(self):
76 90 if self.integration:
77 91 return '{}: {}'.format(
78 92 self.channel_name,
79 93 self.integration.resource.resource_name
80 94 )
81 95
82 96 return '{}: {}'.format(
83 97 self.channel_name,
84 98 self.channel_value
85 99 )
86 100
87 101 def get_dict(self, exclude_keys=None, include_keys=None,
88 102 extended_info=True):
89 103 """
90 104 Returns dictionary with required information that will be consumed by
91 105 angular
92 106 """
93 107 instance_dict = super(AlertChannel, self).get_dict(exclude_keys,
94 108 include_keys)
95 109 exclude_keys_list = exclude_keys or []
96 110 include_keys_list = include_keys or []
97 111
98 112 instance_dict['supports_report_alerting'] = True
99 113 instance_dict['channel_visible_value'] = self.channel_visible_value
100 114
101 115 if extended_info:
102 116 instance_dict['actions'] = [
103 117 rule.get_dict(extended_info=True) for
104 118 rule in self.channel_actions]
105 119
106 120 del instance_dict['channel_json_conf']
107 121
108 122 if self.integration:
109 123 instance_dict[
110 124 'supports_report_alerting'] = \
111 125 self.integration.supports_report_alerting
112 126 d = {}
113 127 for k in instance_dict.keys():
114 128 if (k not in exclude_keys_list and
115 129 (k in include_keys_list or not include_keys)):
116 130 d[k] = instance_dict[k]
117 131 return d
118 132
119 133 def __repr__(self):
120 134 return '<AlertChannel: (%s,%s), user:%s>' % (self.channel_name,
121 135 self.channel_value,
122 136 self.user_name,)
123 137
124 138 def send_digest(self, **kwargs):
125 139 """
126 140 This should implement daily top error report notifications
127 141 """
128 142 log.warning('send_digest NOT IMPLEMENTED')
129 143
130 144 def notify_reports(self, **kwargs):
131 145 """
132 146 This should implement notification of reports that occured in 1 min
133 147 interval
134 148 """
135 149 log.warning('notify_reports NOT IMPLEMENTED')
136 150
137 151 def notify_alert(self, **kwargs):
138 152 """
139 153 Notify user of report/uptime/chart threshold events based on events alert
140 154 type
141 155
142 156 Kwargs:
143 157 application: application that the event applies for,
144 158 event: event that is notified,
145 159 user: user that should be notified
146 160 request: request object
147 161
148 162 """
149 163 alert_name = kwargs['event'].unified_alert_name()
150 164 if alert_name in ['slow_report_alert', 'error_report_alert']:
151 165 self.notify_report_alert(**kwargs)
152 166 elif alert_name == 'uptime_alert':
153 167 self.notify_uptime_alert(**kwargs)
154 168 elif alert_name == 'chart_alert':
155 169 self.notify_chart_alert(**kwargs)
156 170
157 171 def notify_chart_alert(self, **kwargs):
158 172 """
159 173 This should implement report open/close alerts notifications
160 174 """
161 175 log.warning('notify_chart_alert NOT IMPLEMENTED')
162 176
163 177 def notify_report_alert(self, **kwargs):
164 178 """
165 179 This should implement report open/close alerts notifications
166 180 """
167 181 log.warning('notify_report_alert NOT IMPLEMENTED')
168 182
169 183 def notify_uptime_alert(self, **kwargs):
170 184 """
171 185 This should implement uptime open/close alerts notifications
172 186 """
173 187 log.warning('notify_uptime_alert NOT IMPLEMENTED')
174 188
175 189 def get_notification_basic_vars(self, kwargs):
176 190 """
177 191 Sets most common variables used later for rendering notifications for
178 192 channel
179 193 """
180 194 if 'event' in kwargs:
181 195 kwargs['since_when'] = kwargs['event'].start_date
182 196
183 197 url_start_date = kwargs.get('since_when') - timedelta(minutes=1)
184 198 url_end_date = kwargs.get('since_when') + timedelta(minutes=4)
185 199 tmpl_vars = {
186 200 "timestamp": kwargs['since_when'],
187 201 "user": kwargs['user'],
188 202 "since_when": kwargs.get('since_when'),
189 203 "url_start_date": url_start_date,
190 204 "url_end_date": url_end_date
191 205 }
192 206 tmpl_vars["resource_name"] = kwargs['resource'].resource_name
193 207 tmpl_vars["resource"] = kwargs['resource']
194 208
195 209 if 'event' in kwargs:
196 210 tmpl_vars['event_values'] = kwargs['event'].values
197 211 tmpl_vars['alert_type'] = kwargs['event'].unified_alert_name()
198 212 tmpl_vars['alert_action'] = kwargs['event'].unified_alert_action()
199 213 return tmpl_vars
200 214
201 215 def report_alert_notification_vars(self, kwargs):
202 216 tmpl_vars = self.get_notification_basic_vars(kwargs)
203 217 reports = kwargs.get('reports', [])
204 218 tmpl_vars["reports"] = reports
205 219 tmpl_vars["confirmed_total"] = len(reports)
206 220
207 221 tmpl_vars["report_type"] = "error reports"
208 222 tmpl_vars["url_report_type"] = 'report/list'
209 223
210 224 alert_type = tmpl_vars.get('alert_type', '')
211 225 if 'slow_report' in alert_type:
212 226 tmpl_vars["report_type"] = "slow reports"
213 227 tmpl_vars["url_report_type"] = 'report/list_slow'
214 228
215 229 app_url = kwargs['request'].registry.settings['_mail_url']
216 230
217 231 destination_url = kwargs['request'].route_url('/',
218 232 _app_url=app_url)
219 233 if alert_type:
220 234 destination_url += 'ui/{}?resource={}&start_date={}&end_date={}'.format(
221 235 tmpl_vars["url_report_type"],
222 236 tmpl_vars['resource'].resource_id,
223 237 tmpl_vars['url_start_date'].strftime(DATE_FRMT),
224 238 tmpl_vars['url_end_date'].strftime(DATE_FRMT)
225 239 )
226 240 else:
227 241 destination_url += 'ui/{}?resource={}'.format(
228 242 tmpl_vars["url_report_type"],
229 243 tmpl_vars['resource'].resource_id
230 244 )
231 245 tmpl_vars["destination_url"] = destination_url
232 246
233 247 return tmpl_vars
234 248
235 249 def uptime_alert_notification_vars(self, kwargs):
236 250 tmpl_vars = self.get_notification_basic_vars(kwargs)
237 251 app_url = kwargs['request'].registry.settings['_mail_url']
238 252 destination_url = kwargs['request'].route_url('/', _app_url=app_url)
239 253 destination_url += 'ui/{}?resource={}'.format(
240 254 'uptime',
241 255 tmpl_vars['resource'].resource_id)
242 256 tmpl_vars['destination_url'] = destination_url
243 257
244 258 reason = ''
245 259 e_values = tmpl_vars.get('event_values')
246 260
247 261 if e_values and e_values.get('response_time') == 0:
248 262 reason += ' Response time was slower than 20 seconds.'
249 263 elif e_values:
250 264 code = e_values.get('status_code')
251 265 reason += ' Response status code: %s.' % code
252 266
253 267 tmpl_vars['reason'] = reason
254 268 return tmpl_vars
255 269
256 270 def chart_alert_notification_vars(self, kwargs):
257 271 tmpl_vars = self.get_notification_basic_vars(kwargs)
258 272 tmpl_vars['chart_name'] = tmpl_vars['event_values']['chart_name']
259 273 tmpl_vars['action_name'] = tmpl_vars['event_values'].get(
260 274 'action_name') or ''
261 275 matched_values = tmpl_vars['event_values']['matched_step_values']
262 276 tmpl_vars['readable_values'] = []
263 277 for key, value in list(matched_values['values'].items()):
264 278 matched_label = matched_values['labels'].get(key)
265 279 if matched_label:
266 280 tmpl_vars['readable_values'].append({
267 281 'label': matched_label['human_label'],
268 282 'value': value
269 283 })
270 284 tmpl_vars['readable_values'] = sorted(tmpl_vars['readable_values'],
271 285 key=lambda x: x['label'])
272 286 start_date = convert_date(tmpl_vars['event_values']['start_interval'])
273 287 end_date = None
274 288 if tmpl_vars['event_values'].get('end_interval'):
275 289 end_date = convert_date(tmpl_vars['event_values']['end_interval'])
276 290
277 291 app_url = kwargs['request'].registry.settings['_mail_url']
278 292 destination_url = kwargs['request'].route_url('/', _app_url=app_url)
279 293 to_encode = {
280 294 'resource': tmpl_vars['event_values']['resource'],
281 295 'start_date': start_date.strftime(DATE_FRMT),
282 296 }
283 297 if end_date:
284 298 to_encode['end_date'] = end_date.strftime(DATE_FRMT)
285 299
286 300 destination_url += 'ui/{}?{}'.format(
287 301 'logs',
288 302 urllib.parse.urlencode(to_encode)
289 303 )
290 304 tmpl_vars['destination_url'] = destination_url
291 305 return tmpl_vars
@@ -1,155 +1,160 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 4 #
5 5 # Licensed under the Apache License, Version 2.0 (the "License");
6 6 # you may not use this file except in compliance with the License.
7 7 # You may obtain a copy of the License at
8 8 #
9 9 # http://www.apache.org/licenses/LICENSE-2.0
10 10 #
11 11 # Unless required by applicable law or agreed to in writing, software
12 12 # distributed under the License is distributed on an "AS IS" BASIS,
13 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 14 # See the License for the specific language governing permissions and
15 15 # limitations under the License.
16 16
17 17 import sqlalchemy as sa
18 18 import logging
19 19
20 20 from datetime import datetime
21 21 from appenlight.models import Base, get_db_session
22 22 from appenlight.models.services.report_stat import ReportStatService
23 23 from appenlight.models.resource import Resource
24 24 from appenlight.models.integrations import IntegrationException
25 25 from pyramid.threadlocal import get_current_request
26 26 from sqlalchemy.dialects.postgresql import JSON
27 27 from ziggurat_foundations.models.base import BaseModel
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 class Event(Base, BaseModel):
33 33 __tablename__ = 'events'
34 34
35 35 types = {'error_report_alert': 1,
36 36 'slow_report_alert': 3,
37 37 'comment': 5,
38 38 'assignment': 6,
39 39 'uptime_alert': 7,
40 40 'chart_alert': 9}
41 41
42 42 statuses = {'active': 1,
43 43 'closed': 0}
44 44
45 45 id = sa.Column(sa.Integer, primary_key=True)
46 46 start_date = sa.Column(sa.DateTime, default=datetime.utcnow)
47 47 end_date = sa.Column(sa.DateTime)
48 48 status = sa.Column(sa.Integer, default=1)
49 49 event_type = sa.Column(sa.Integer, default=1)
50 50 origin_user_id = sa.Column(sa.Integer(), sa.ForeignKey('users.id'),
51 51 nullable=True)
52 52 target_user_id = sa.Column(sa.Integer(), sa.ForeignKey('users.id'),
53 53 nullable=True)
54 54 resource_id = sa.Column(sa.Integer(),
55 55 sa.ForeignKey('resources.resource_id'),
56 56 nullable=True)
57 57 target_id = sa.Column(sa.Integer)
58 58 target_uuid = sa.Column(sa.Unicode(40))
59 59 text = sa.Column(sa.UnicodeText())
60 60 values = sa.Column(JSON(), nullable=False, default=None)
61 61
62 62 def __repr__(self):
63 63 return '<Event %s, app:%s, %s>' % (self.unified_alert_name(),
64 64 self.resource_id,
65 65 self.unified_alert_action())
66 66
67 67 @property
68 68 def reverse_types(self):
69 69 return dict([(v, k) for k, v in self.types.items()])
70 70
71 71 def unified_alert_name(self):
72 72 return self.reverse_types[self.event_type]
73 73
74 74 def unified_alert_action(self):
75 75 event_name = self.reverse_types[self.event_type]
76 76 if self.status == Event.statuses['closed']:
77 77 return "CLOSE"
78 78 if self.status != Event.statuses['closed']:
79 79 return "OPEN"
80 80 return event_name
81 81
82 82 def send_alerts(self, request=None, resource=None, db_session=None):
83 83 """" Sends alerts to applicable channels """
84 84 db_session = get_db_session(db_session)
85 85 db_session.flush()
86 86 if not resource:
87 87 resource = Resource.by_resource_id(self.resource_id)
88 88 if not request:
89 89 request = get_current_request()
90 90 if not resource:
91 91 return
92 92 users = set([p.user for p in resource.users_for_perm('view')])
93 93 for user in users:
94 94 for channel in user.alert_channels:
95 if not channel.channel_validated or not channel.send_alerts:
95 matches_resource = not channel.resources or resource in [r.resource_id for r in channel.resources]
96 if (
97 not channel.channel_validated or
98 not channel.send_alerts or
99 not matches_resource
100 ):
96 101 continue
97 102 else:
98 103 try:
99 104 channel.notify_alert(resource=resource,
100 105 event=self,
101 106 user=user,
102 107 request=request)
103 108 except IntegrationException as e:
104 109 log.warning('%s' % e)
105 110
106 111 def validate_or_close(self, since_when, db_session=None):
107 112 """ Checks if alerts should stay open or it's time to close them.
108 113 Generates close alert event if alerts get closed """
109 114 event_types = [Event.types['error_report_alert'],
110 115 Event.types['slow_report_alert']]
111 116 app = Resource.by_resource_id(self.resource_id)
112 117 if self.event_type in event_types:
113 118 total = ReportStatService.count_by_type(
114 119 self.event_type, self.resource_id, since_when)
115 120 if Event.types['error_report_alert'] == self.event_type:
116 121 threshold = app.error_report_threshold
117 122 if Event.types['slow_report_alert'] == self.event_type:
118 123 threshold = app.slow_report_threshold
119 124
120 125 if total < threshold:
121 126 self.close()
122 127
123 128 def close(self, db_session=None):
124 129 """
125 130 Closes an event and sends notification to affected users
126 131 """
127 132 self.end_date = datetime.utcnow()
128 133 self.status = Event.statuses['closed']
129 134 log.warning('ALERT: CLOSE: %s' % self)
130 135 self.send_alerts()
131 136
132 137 def text_representation(self):
133 138 alert_type = self.unified_alert_name()
134 139 text = ''
135 140 if 'slow_report' in alert_type:
136 141 text += 'Slow report alert'
137 142 if 'error_report' in alert_type:
138 143 text += 'Exception report alert'
139 144 if 'uptime_alert' in alert_type:
140 145 text += 'Uptime alert'
141 146 if 'chart_alert' in alert_type:
142 147 text += 'Metrics value alert'
143 148
144 149 alert_action = self.unified_alert_action()
145 150 if alert_action == 'OPEN':
146 151 text += ' got opened.'
147 152 if alert_action == 'CLOSE':
148 153 text += ' got closed.'
149 154 return text
150 155
151 156 def get_dict(self, request=None):
152 157 dict_data = super(Event, self).get_dict()
153 158 dict_data['text'] = self.text_representation()
154 159 dict_data['resource_name'] = self.resource.resource_name
155 160 return dict_data
General Comments 2
Under Review
author

Auto status change to "Under Review"

Rejected

Please use: https://github.com/Appenlight/appenlight to contribute :) Thanks !

You need to be logged in to leave comments. Login now