考虑到 alertmanager的配置文件已达上千行配置,随着后续的要求越来越复杂,告警类别的要求越来越多样化,逐渐诞生了自己写一个告警系统接入alertmanager的想法。

效果图如下:

image-20240207141007283

image-20240207145209023

image-20240207145505490

image-20240207145659556

后端代码

models.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from django.db import models
from django.utils.translation import gettext_lazy as _
# from django.conf import settings
from apps.models import AutoFieldModel


TYPE = (("dingtalk", "钉钉"), ("sms", "短信"))


class Alerts(AutoFieldModel):
STATE = (("pending", "等待"), ("firing", "告警"))
summary = models.CharField(_("alert name"), max_length=255)
description = models.CharField(_("alert description"), max_length=255)
labels = models.JSONField(_("labels"), default=dict)
state = models.CharField(_("status"), max_length=64, choices=STATE)
active_at = models.DateTimeField(_("actived timestamp of record"), null=True, blank=True)
value = models.FloatField(_("value"), null=True, blank=True)

username = models.CharField(_("username"), null=True, blank=True, max_length=32)
comment = models.CharField(_("comment"), null=True, blank=True, max_length=255)
silenced_id = models.CharField(_("silenced_id"), null=True, blank=True, max_length=64)
silenced_expire_at = models.DateTimeField(_("silence timestamp"), null=True, blank=True)


class NotifyTemplate(models.Model):
name = models.CharField(_("template name"), max_length=64, null=True, blank=True)
jinja_template = models.TextField(_("jinja2 template"), null=True, blank=True)


class NotifyRouter(models.Model):
name = models.CharField(_("name"), max_length=64, null=True, blank=True)
notify_type = models.CharField(_("notify type"), max_length=32, choices=TYPE)
rules = models.JSONField(_("match rules"), default=dict)
dingtalk_access = models.CharField(_("dingtalk access_token"), max_length=128, null=True, blank=True)
dingtalk_token = models.CharField(_("dingtalk signature"), max_length=128, null=True, blank=True, default="")
send_resolved = models.BooleanField(_("send resolved or not"), default=True)
at_someone_or_phone = models.JSONField(_("at someone or phone"), null=True, blank=True, default=list)
created_at = models.DateTimeField(_("creation timestamp of record"), auto_now_add=True)
template = models.ForeignKey(to=NotifyTemplate, verbose_name=_("template"), null=True, on_delete=models.SET_NULL)


class NotifyRecord(models.Model):
alertname = models.CharField(_("alert name"), max_length=64, null=True, blank=True)
notify_type = models.CharField(_("notify type"), max_length=32, choices=TYPE)
content = models.TextField(_("content"), null=True, blank=True)
router = models.ForeignKey(to=NotifyRouter, verbose_name=_("router"), null=True, on_delete=models.SET_NULL)
created_at = models.DateTimeField(_("creation timestamp of record"), auto_now_add=True)

serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import requests
from rest_framework import serializers
from django.conf import settings
from apps.alerts.models import Alerts, NotifyTemplate, NotifyRouter
from apps.serializers import ModelSerializer, NestedAction


def generate_silence_id(attrs):
"""静默告警项"""
alertmanager_api_url = settings.ALERTMANAGER_URL + "/api/v2/silences"
matchers = []
for key, value in attrs["labels"].items():
matchers.append({"isRegex": False, "name": key, "value": value})
data = {
"matchers": matchers,
"startsAt": attrs["silenced_start_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
"endsAt": attrs["silenced_expire_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
"comment": attrs["comment"],
"createdBy": attrs["username"]
}
resp = requests.post(alertmanager_api_url, json=data)
result = resp.json()["silenceID"]
return result


class AlertsSerializer(ModelSerializer):
# write_only 不需要写数据库
is_silence = serializers.BooleanField(required=True, write_only=True)
silenced_start_at = serializers.DateTimeField(required=False, write_only=True)

def validate(self, attrs):
if attrs["is_silence"]:
attrs["silenced_id"] = generate_silence_id(attrs)
return attrs

class Meta:
model = Alerts
exclude = ["value"]


class NotifyTemplateSerializer(ModelSerializer):
class Meta:
model = NotifyTemplate
fields = "__all__"


class TestNotifyTemplateSerializer(ModelSerializer):
"""测试告警模板"""
at = serializers.CharField(max_length=None, allow_null=True, required=False)
content = serializers.CharField(max_length=None, allow_null=False, required=False)
access_token = serializers.CharField(max_length=None, allow_null=False, required=False)
secret = serializers.CharField(max_length=None, allow_null=True, required=False)

class Meta:
model = NotifyTemplate
fields = ["at", "content", "access_token", "secret", "name", "jinja_template"]


class NotifyRouterSerializer(ModelSerializer):
""" 外键修改需要增加该字段,否则无法修改"""
template = serializers.PrimaryKeyRelatedField(
allow_null=False, many=False,
queryset=NotifyTemplate.objects.all())

def to_representation(self, instance):
obj = instance.template
ret = super(NotifyRouterSerializer, self).to_representation(instance)
if obj:
ret["template"] = {"id": obj.id, "name": obj.name}
else:
ret["template"] = None
return ret

class Meta:
model = NotifyRouter
exclude = ["created_at"]

filters.py

1
2
3
4
5
6
7
8
9
10
11
from django_filters import rest_framework as filters
from apps.alerts import models


class AlertsFilter(filters.FilterSet):
is_deleted = filters.BooleanFilter(label='是否删除', field_name='deleted_at', lookup_expr='isnull', exclude=True)
is_silenced = filters.BooleanFilter(label='是否静默', field_name='silenced_id', lookup_expr='isnull', exclude=True)

class Meta:
model = models.Alerts
fields = ['description', 'state', 'summary']

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from rest_framework import viewsets
from apps.alerts.models import Alerts, NotifyTemplate, NotifyRouter
from apps.alerts import serializers
from apps.alerts import filters
from apps.alerts.mixins import CheckNotifyTemplateViewMixin, AlertSendViewMixin


class AlertsViewSet(viewsets.ModelViewSet, AlertSendViewMixin):
resource_name = "promethus_alerts"
queryset = Alerts.objects.all()
serializer_class = serializers.AlertsSerializer
lookup_field = 'id'
filter_class = filters.AlertsFilter
search_fields = ['description', 'summary']


class NotifyTemplateViewSet(viewsets.ModelViewSet, CheckNotifyTemplateViewMixin):
resource_name = "alerts_template"
queryset = NotifyTemplate.objects.all()
serializer_class = serializers.NotifyTemplateSerializer
lookup_field = 'id'
search_fields = ['name', 'dingtalk_url_or_phone']


class NotifyRouterViewSet(viewsets.ModelViewSet):
resource_name = "alerts_router"
queryset = NotifyRouter.objects.all()
serializer_class = serializers.NotifyRouterSerializer
lookup_field = 'id'
filterset_fields = ('notify_type',)
search_fields = ['name']

mixins.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import json
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from apps.alerts.utils import dingtalk_send_notify, render_alert, send_notify_main


class CheckNotifyTemplateViewMixin:
"""测试告警模板 发送告警结果样式"""
@action(methods=['post'], detail=False)
def check_template(self, request):
content = request.data.get("content")
access_token = request.data.get("access_token")
jinja_template = request.data.get("jinja_template")
secret = request.data.get("secret", "")
at = request.data.get("at").split(",") if request.data.get("at") else None

if all([content, access_token, jinja_template]):
# send_notify(json.loads(content), notify_type="dingtalk")
# result = {"errcode": 0, "errmsg": "ok"}
markdown = render_alert(jinja_template, json.loads(content))
result = dingtalk_send_notify(markdown, access_token=access_token, secret=secret, at=at)
return Response(result)
return Response(status=status.HTTP_400_BAD_REQUEST, data={"errcode": 400, "errmsg": "请求参数错误"})


class AlertSendViewMixin:
"""发送告警"""
@action(methods=['post'], detail=False)
def send(self, request):
content = request.data
notify_type = request.query_params.get("notify_type")
send_notify_main(content, notify_type=notify_type)
result = {"errcode": 0, "errmsg": "ok"}
return Response(result)

sms.py

发送短信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import uuid
from django.conf import settings
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.profile import region_provider
from aliyunsdkcore.request import RpcRequest


# 注意:不要更改
REGION = "cn-hangzhou"
PRODUCT_NAME = "Dysmsapi"
DOMAIN = "dysmsapi.aliyuncs.com"

acs_client = AcsClient(settings.SMS["access_key_id"], settings.SMS["access_key_secret"], REGION)
region_provider.add_endpoint(PRODUCT_NAME, REGION, DOMAIN)


class SendSmsRequest(RpcRequest):
def __init__(self):
RpcRequest.__init__(self, 'Dysmsapi', '2017-05-25', 'SendSms')

def get_TemplateCode(self):
return self.get_query_params().get('TemplateCode')

def set_TemplateCode(self, TemplateCode):
self.add_query_param('TemplateCode', TemplateCode)

def get_PhoneNumbers(self):
return self.get_query_params().get('PhoneNumbers')

def set_PhoneNumbers(self, PhoneNumbers):
self.add_query_param('PhoneNumbers', PhoneNumbers)

def get_SignName(self):
return self.get_query_params().get('SignName')

def set_SignName(self, SignName):
self.add_query_param('SignName', SignName)

def get_ResourceOwnerAccount(self):
return self.get_query_params().get('ResourceOwnerAccount')

def set_ResourceOwnerAccount(self, ResourceOwnerAccount):
self.add_query_param('ResourceOwnerAccount', ResourceOwnerAccount)

def get_TemplateParam(self):
return self.get_query_params().get('TemplateParam')

def set_TemplateParam(self, TemplateParam):
self.add_query_param('TemplateParam', TemplateParam)

def get_ResourceOwnerId(self):
return self.get_query_params().get('ResourceOwnerId')

def set_ResourceOwnerId(self, ResourceOwnerId):
self.add_query_param('ResourceOwnerId', ResourceOwnerId)

def get_OwnerId(self):
return self.get_query_params().get('OwnerId')

def set_OwnerId(self, OwnerId):
self.add_query_param('OwnerId', OwnerId)

def get_SmsUpExtendCode(self):
return self.get_query_params().get('SmsUpExtendCode')

def set_SmsUpExtendCode(self, SmsUpExtendCode):
self.add_query_param('SmsUpExtendCode', SmsUpExtendCode)

def get_OutId(self):
return self.get_query_params().get('OutId')

def set_OutId(self, OutId):
self.add_query_param('OutId', OutId)


def send_sms(phone_numbers, template_param=None):
business_id = uuid.uuid4()
sms_request = SendSmsRequest()
sms_request.set_TemplateCode(settings.SMS["template_code"]) # 短信模板变量参数
if template_param is not None:
sms_request.set_TemplateParam(template_param)
sms_request.set_OutId(business_id) # 设置业务请求流水号,必填。
sms_request.set_SignName(settings.SMS["sign_name"]) # 短信签名
sms_request.set_PhoneNumbers(phone_numbers) # 短信发送的号码列表,必填。
sms_response = acs_client.do_action_with_exception(sms_request) # 调用短信发送接口,返回json

return sms_response

utils.py

重点关注,发送告警的规则都在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import logging
import pytz
import re
from jinja2 import Template
from datetime import datetime
from dateutil.parser import parse

from apps.tasks.tasks import DingTalkBot
from apps.alerts.models import NotifyTemplate, NotifyRouter, NotifyRecord
from apps.inventory.models import RDSInstance, RedisInstance, Instance
from apps.ldap import const as C
from apps.ldap import connection
from apps.ldap.utils import format_search_filter, formalize_ldap_attrs
from apps.alerts.sms import send_sms


def get_cst_time(timestamp) -> str:
"""time: 2023-12-08T18:21:59.116003027Z"""
dt = parse(timestamp)
target_timezone = pytz.timezone("Asia/Shanghai") # +08:00 时区
dt = dt.replace(tzinfo=pytz.utc).astimezone(target_timezone)
formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S")
return formatted_time


def alert_duration(start_time, end_time=None) -> str:
start_time = parse(start_time)
if not end_time:
# 获取当前时间的带有时区信息的 datetime 对象
end_time = datetime.now(pytz.utc)
else:
end_time = parse(end_time)
return end_time - start_time


def string_match(rule_str, param) -> bool:
"""取反 匹配"""
qu_fan = rule_str.count('!')
pattern = r"^" + rule_str.lstrip('!')
pattern_result = bool(re.match(pattern, param))

if qu_fan:
return not pattern_result
return pattern_result


def rule_match(labels, rules):
"""规则匹配"""
bool_list = []
for key, value in rules.items():
if key in labels:
if "|" not in value:
bool_list.append(string_match(value, labels[key]))
else:
batch_match = [string_match(v, labels[key]) for v in value.split("|")]
bool_list.append(any(batch_match))
else:
bool_list.append(False)
return bool_list



def render_alert(jinja_template: str, content: dict):
"""根据模板 渲染告警"""
template = Template(jinja_template)
dingtalk_markdown = template.render(content=content, alert_duration=alert_duration, format_time=get_cst_time)
return dingtalk_markdown


def dingtalk_send_notify(markdown: str, access_token: str, secret: str, at: list = None):
dingtalk_bot = DingTalkBot(access_token=access_token, secret=secret)
result = dingtalk_bot.do_request_markdown(markdown, title="Prometheus告警", at_mobiles=at)
return result


def get_maintainer_mobile(labels: dict):
"""将维护人花名, 通过ldap获取到对应的手机号,方便发短信或者钉钉@人员"""
names = []
mobiles = []

if labels.get("dingtalk") == 'performance':
return mobiles

if "rds" in labels.get("kind", ''):
obj = RDSInstance.objects.get(rds_id=labels["instanceId"], deleted_at__isnull=True)
names = [obj.charge_user[i:i + 2] for i in range(0, len(obj.charge_user), 3)]

if "ecs" in labels.get("kind", ''):
if "instance" in labels.keys():
ip = labels["instance"].split(':')[0]
obj = Instance.objects.get(private_ip_addr=ip, deleted_at__isnull=True)
else:
try:
obj = Instance.objects.get(name=labels["node"], deleted_at__isnull=True)
except Instance.DoesNotExist:
obj = None

if obj:
names = [obj.charge_user[i:i + 2] for i in range(0, len(obj.charge_user), 3)]

if "redis" in labels.get("kind", ''):
if "redis_id" in labels.keys():
obj = RedisInstance.objects.get(redis_id=labels["redis_id"], deleted_at__isnull=True)
if "instanceId" in labels.keys():
obj = RedisInstance.objects.get(redis_id=labels["instanceId"], deleted_at__isnull=True)
names = [obj.charge_user[i:i + 2] for i in range(0, len(obj.charge_user), 3)]

if labels.get("maintainer"):
names.extend(labels["maintainer"].split(","))

with connection() as c:
for name in names:
ret = c._get_single_entry_attr(
C.ldap_user_search_base,
format_search_filter({"displayName": name}, C.ldap_user_default_object_cls),
attributes=['displayName', 'mobile']
)
if not ret:
logging.error(f'{name} not found in ldap')
continue

mobiles.append(ret["attributes"]["mobile"][0].lstrip("+86-"))
return mobiles


def render_alert_and_send(content: dict, router: NotifyRouter, jinja_template: str, notify_type: str):
markdown_alerts = ""
markdown = ""
maintainers = []
alertname = content["groupLabels"]["alertname"] if "groupLabels" in content.keys() else content["alerts"][0]["labels"]["alertname"]

for i, alert in enumerate(content["alerts"]):
if all(rule_match(alert["labels"], router.rules)):
if not router.send_resolved and alert["status"] == "resolved":
continue
markdown = render_alert(jinja_template, {"alerts": [alert]})
mobiles = get_maintainer_mobile(alert["labels"])
if alert["labels"].get("dingtalk") != "performance":
mobiles.extend(router.at_someone_or_phone)
maintainers.extend(mobiles)
if notify_type == "dingtalk":
markdown += "\n###### ✨[点我屏蔽告警](https://alert.in.example.com/#/PromeAlerts)✨\n-------\n"
if mobiles:
markdown += f"\n@{' @'.join(set(mobiles))}\n-------\n\n------\n"

markdown_alerts += markdown
# 防止钉钉超过字数而无法发送告警
if i % 9 == 0 and i > 0 and notify_type == "dingtalk":
dingtalk_send_notify(markdown_alerts, router.dingtalk_access, router.dingtalk_token, list(set(maintainers)))
markdown_alerts = ""
markdown = ""
maintainers = []
NotifyRecord.objects.create(
alert_name=alertname,
notify_type=notify_type,
content=markdown_alerts,
router=router
)

if len(markdown_alerts) > 0:
if notify_type == "dingtalk":
dingtalk_send_notify(markdown_alerts, router.dingtalk_access, router.dingtalk_token, list(set(maintainers)))
if notify_type == "sms":
for mobile in maintainers:
send_sms(mobile.strip("+86-"), template_param={"code": markdown_alerts})
NotifyRecord.objects.create(
alertname=alertname,
notify_type=notify_type,
content=markdown_alerts,
router=router
)


def send_notify_main(content: dict, notify_type: str):
routers_objects = NotifyRouter.objects.filter(notify_type=notify_type)
for router in routers_objects:
jinja_template = NotifyTemplate.objects.get(pk=router.template_id)
render_alert_and_send(content, router, jinja_template.jinja_template, notify_type)

前端代码

  1. 告警路由vue组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
<template>
<div class="app-container">
<div class="filter-container">
<el-input
v-model="listQuery.search"
placeholder="路由名称"
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
/>
<el-select
v-model="listQuery.notify_type"
placeholder="告警媒介"
style="width: 120px"
class="filter-item"
clearable
>
<el-option
v-for="item in notifyTypeOption"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
<el-button
v-waves
class="filter-item"
type="primary"
style="margin-left: 10px"
icon="el-icon-search"
circle
@click="handleFilter"
/>
<el-button
class="filter-item"
style="margin-left: 10px"
type="primary"
icon="el-icon-edit-outline"
@click="handleCreate"
>新建规则</el-button>
</div>
<el-table
v-loading="listLoading"
:data="list"
stripe
border
fit
highlight-current-row
style="width: 100%"
element-loading-text="加载中"
>
<el-table-column align="center" label="路由名称" width="150">
<template v-slot="{ row }">
<span v-if="row.name">
{{ row.name }}
</span>
</template>
</el-table-column>
<el-table-column align="center" label="关联模板" width="100">
<template v-slot="{ row }">
<span v-if="row.template.name">
{{ row.template.name }}
</span>
</template>
</el-table-column>
<el-table-column align="center" label="告警媒介" min-width="60">
<template v-slot="{ row }">
{{ row.notify_type }}
</template>
</el-table-column>
<el-table-column align="center" label="匹配规则" min-width="200">
<template v-slot="{ row }">
<span v-for="(v, k) in row.rules" :key="k">
<el-tag effect="dark">{{ k }}={{ v }}</el-tag>
</span>
</template>
</el-table-column>
<el-table-column align="center" label="通知对象" min-width="200">
<template v-slot="{ row }">
{{ row.dingtalk_access }}
</template>
</el-table-column>
<el-table-column align="center" label="手机" min-width="200">
<template v-slot="{ row }">
<span v-for="(v, k) in row.at_someone_or_phone" :key="k">
<el-tag effect="dark">{{ v }}</el-tag>
</span>
</template>
</el-table-column>
<el-table-column align="center" label="是否发送恢复告警" width="80">
<template v-slot="{ row }">
<el-tag v-if="row.send_resolved" type="success"></el-tag>
<el-tag v-else type="warning"></el-tag>
</template>
</el-table-column>
<el-table-column
align="center"
label="Actions"
class-name="small-padding fixed-width"
width="180px"
fixed="right"
>
<template v-slot="{ row, $index }">
<el-button
icon="el-icon-edit"
type="primary"
size="small"
plain
@click="handleUpdate(row)"
>编辑
</el-button>
<el-button
icon="el-icon-delete"
type="danger"
size="small"
plain
@click="handleDelete(row, $index)"
>删除
</el-button>
</template>
</el-table-column>
</el-table>

<pagination
v-show="total > 0"
:total="total"
:page.sync="listQuery.page"
:limit.sync="listQuery.i_really_want_page_size"
@pagination="getList"
/>

<el-dialog
v-el-drag-dialog
:title="textMap[dialogStatus]"
:visible.sync="dialogFormVisible"
center
@dragDialog="this.$refs.select.blur()"
>
<el-form
ref="dataForm"
:rules="rules"
:model="temp"
label-position="right"
label-width="120px"
style="margin: 0 auto"
size="small"
>
<el-form-item label="路由名称" prop="name">
<el-input v-model="temp.name" />
</el-form-item>
<el-form-item label="通知类型" prop="notify_type" required>
<el-select
v-model="temp.notify_type"
placeholder="平台"
clearable
>
<el-option
v-for="item in notifyTypeOption"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关联模板" prop="notify_type" required>
<el-select
v-model="temp.template"
style="display: block"
filterable
remote
reserve-keyword
value-key="id"
placeholder="Type to search"
:remote-method="asyncGetAlertTemplateList"
@remove-tag="templateListChange"
>
<el-option
v-for="(item) in templateList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<template v-if="temp.notify_type ==='dingtalk'">
<el-form-item label="机器人Access" prop="dingtalk_access">
<el-input v-model="temp.dingtalk_access" />
</el-form-item>
<el-form-item label="机器人Secret" prop="dingtalk_token">
<el-input v-model="temp.dingtalk_token" />
</el-form-item>
</template>
<el-form-item label="@人员/手机" prop="at_someone_or_phone">
<el-select
v-model="temp.at_someone_or_phone"
style="display: block"
reserve-keyword
multiple
filterable
clearable
allow-create
>
<el-option
v-for="item in mobileList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="标签">
<div v-for="(item, index) in tagArr" :key="index" class="filter-item">
<!-- 删除小图标 -->
<i
v-show="show(index)"
class="el-icon-remove-outline deleted"
@click="deleteItem(index)"
/>
<!-- 输入框v-model绑定数组 -->
<el-input v-model.trim="item.label" placeholder="如 app" />
<el-input v-model.trim="item.value" placeholder="如 nginx" />
</div>
<el-button type="text" size="small" @click="addItem">+增加</el-button>
</el-form-item>
<el-form-item label="发送恢复通知" prop="send_resolved">
<el-switch v-model="temp.send_resolved" active-text="是" inactive-text="否" />
</el-form-item>
</el-form>

<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> 取消 </el-button>
<el-button type="primary" @click="dialogStatus === 'create' ? createData() : updateData()"> 确认 </el-button>
</div>
</el-dialog>

</div>
</template>

<script>
import { createAlertRouter, listAlertRouter, listAlertTemplate, updateAlertRouter, deleteAlertRouter } from '@/api/alerts_list'
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
import elDragDialog from '@/directive/el-drag-dialog' // base on element-ui
import waves from '@/directive/waves'

export default {
name: 'NotifyTemplate',
components: { Pagination },
directives: { elDragDialog, waves },
data() {
return {
list: [],
total: 0,
listLoading: false,
listQuery: {
page: 1,
i_really_want_page_size: 10,
search: undefined,
state: undefined,
is_deleted: false,
notify_type: undefined,
sort: undefined
},
notifyTypeOption: [
{ text: '钉钉', value: 'dingtalk' },
{ text: '短信', value: 'sms' }
],
dialogStatus: '',
textMap: {
update: '编辑',
create: '创建'
},
temp: {
name: undefined,
notify_type: undefined,
rules: {},
dingtalk_access: undefined,
dingtalk_token: undefined,
send_resolved: true,
template: undefined,
at_someone_or_phone: undefined
},
tagArr: [],
dialogFormVisible: false,
templateListQuery: {
show_fewer: true,
no_pagination: true,
search: undefined
},
mobileList: [],
templateList: [],
templateListRemove: [],
rules: {}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.listLoading = true
listAlertRouter(this.listQuery).then((response) => {
this.list = response.data.results
this.total = response.data.count
this.listLoading = false
})
},
asyncGetAlertTemplateList(q) {
if (q.trim() !== '') {
this.instanceListQuery.search = q.trim()
}
listAlertTemplate(this.templateListQuery).then((response) => {
this.templateList = response.data
})
},
templateListChange(v) {
this.templateListRemove.push(v)
},
handleFilter() {
this.listQuery.page = 1
this.getList()
},
resetTemp() {
this.temp = {
name: undefined,
notify_type: undefined,
rules: {},
dingtalk_access: undefined,
dingtalk_token: undefined,
send_resolved: true,
template: undefined,
at_someone_or_phone: undefined
}
this.tagArr = []
},
handleDelete(row, index) {
this.$confirm('此操作将删除该路由规则, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteAlertRouter(row.id).then(() => {
this.$notify({
title: 'Success',
message: 'Delete Successfully',
type: 'success',
duration: 2000
})
this.list.splice(index, 1)
})
})
},
handleUpdate(row) {
this.resetTemp()
this.temp = Object.assign({}, row) // copy obj
// this.temp.template = this.temp.template.name
for (const key in this.temp.rules) {
this.tagArr.push({ label: key, value: this.temp.rules[key] })
}
this.dialogStatus = 'update'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs.dataForm.clearValidate()
})
},
updateData() {
this.$refs.dataForm.validate((valid) => {
this.handleRules()
if (valid) {
const tempData = Object.assign({}, this.temp)
tempData.template = tempData.template.id
updateAlertRouter(this.temp.id, tempData).then((response) => {
const index = this.list.findIndex((v) => v.id === this.temp.id)
this.list.splice(index, 1, response.data)
this.dialogFormVisible = false
this.$notify({
title: 'Success',
message: 'Update Successfully',
type: 'success',
duration: 2000
})
})
}
})
},
handleCreate() {
this.dialogFormVisible = true
this.dialogStatus = 'create'
this.resetTemp()
this.tagArr = [{ label: '', value: '' }]
this.$nextTick(() => {
this.$refs.dataForm.clearValidate()
})
},
createData() {
this.$refs.dataForm.validate((valid) => {
this.handleRules()
if (valid) {
const tempData = Object.assign({}, this.temp)
tempData.template = tempData.template.id
createAlertRouter(this.temp).then((response) => {
this.list.unshift(response.data)
this.dialogFormVisible = false
this.$notify({
title: 'Success',
message: 'Created Successfully',
type: 'success',
duration: 2000
})
})
}
})
},
show(i) {
return !(i < 1)
},
deleteItem(i) {
delete this.temp.rules[this.tagArr[i].label]
this.tagArr.splice(i, 1)
},
addItem() {
this.tagArr.push({ label: '', value: '' })
},
handleRules() {
for (let i = 0; i < this.tagArr.length; i++) {
if (this.tagArr[i].label !== '' && this.tagArr[i].value !== '') {
this.temp.rules[this.tagArr[i].label] = this.tagArr[i].value
}
}
}
}
}
</script>

<style scoped>
.table-expand label {
width: 90px;
color: #99a9bf;
}
@media screen and (max-width: 500px) {
.el-dialog__wrapper .el-dialog {
width: 300px !important;
.el-dialog__body{
padding: 10px 20px!important;
.el-form-item__label{
width: 68px!important;
}
.el-select,.el-input{
width: 180px!important;
}
}
}
}
</style>
  1. 告警模板组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
<template>
<div class="app-container">
<div class="filter-container">
<el-input
v-model="listQuery.search"
placeholder="模板名称"
style="width: 200px"
class="filter-item"
@keyup.enter.native="handleFilter"
/>
<el-button
v-waves
class="filter-item"
type="primary"
style="margin-left: 10px"
icon="el-icon-search"
circle
@click="handleFilter"
/>
<el-button
class="filter-item"
style="margin-left: 10px"
type="primary"
icon="el-icon-edit-outline"
@click="handleCreate"
>新建模板</el-button>
</div>
<el-table
v-loading="listLoading"
:data="list"
stripe
border
fit
highlight-current-row
style="width: 100%"
element-loading-text="加载中"
>
<el-table-column align="center" label="ID" width="50">
<template v-slot="{ row }">
{{ row.id }}
</template>
</el-table-column>
<el-table-column align="center" label="模板名称" width="80">
<template v-slot="{ row }">
{{ row.name }}
</template>
</el-table-column>
<!--
<el-table-column align="center" label="模板内容" min-width="200">
<template v-slot="{ row }">
<el-popover title="详细内容" trigger="click" placement="top" width="800">
<p v-html="row.jinja_template" />
<div slot="reference">
<el-button type="plain" style="white-space: nowrap;overflow: hidden;text-overflow:ellipsis;">详细内容</el-button>
</div>
</el-popover>
</template>
</el-table-column>
-->
<el-table-column align="center" label="模板内容" min-width="200">
<template v-slot="{ row }">
<p v-html="row.jinja_template" />
</template>
</el-table-column>
<el-table-column
align="center"
label="Actions"
class-name="small-padding fixed-width"
width="180px"
fixed="right"
>
<template v-slot="{ row, $index }">
<el-button
icon="el-icon-edit"
type="primary"
size="small"
plain
@click="handleUpdate(row)"
>编辑
</el-button>
<el-button
icon="el-icon-delete"
type="danger"
size="small"
plain
@click="handleDelete(row, $index)"
>删除
</el-button>
</template>
</el-table-column>
</el-table>

<pagination
v-show="total > 0"
:total="total"
:page.sync="listQuery.page"
:limit.sync="listQuery.i_really_want_page_size"
@pagination="getList"
/>

<el-dialog
v-el-drag-dialog
:title="textMap[dialogStatus]"
:visible.sync="dialogFormVisible"
center
size="80%"
@dragDialog="this.$refs.select.blur()"
>
<el-form
ref="dataForm"
:rules="rules"
:model="temp"
label-position="right"
label-width="120px"
style="margin: 0 auto"
size="small"
>
<el-row>
<el-col :span="24">
<el-form-item label="模板名称" prop="name" required>
<el-input v-model="temp.name" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="模板内容" prop="jinja_template" required>
<el-input v-model="temp.jinja_template" type="textarea" :autosize="true" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="机器人Access" prop="access_token">
<el-input v-model="temp.access_token" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="机器人Secret" prop="secret">
<el-input v-model="temp.secret" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="@某人" prop="at_someone">
<el-input v-model="temp.at" placeholder="输入手机号" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="消息内容" prop="content">
<el-input v-model="temp.content" type="textarea" :autosize="true" placeholder="{}" />
</el-form-item>
</el-col>
</el-row>
</el-form>

<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> 取消 </el-button>
<el-button type="warning" @click="checkTemplate()"> 模板测试 </el-button>
<el-button type="primary" @click="dialogStatus === 'create' ? createData() : updateData()"> 确认 </el-button>
</div>
</el-dialog>

</div>

</template>

<script>
import {
listAlertTemplate,
updateAlertTemplate,
checkAlertTemplate,
createAlertTemplate,
deleteAlertTemplate
} from '@/api/alerts_list'
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
import elDragDialog from '@/directive/el-drag-dialog' // base on element-ui
import waves from '@/directive/waves'

export default {
name: 'NotifyTemplate',
components: { Pagination },
directives: { elDragDialog, waves },
data() {
return {
list: [],
total: 0,
listLoading: false,
listQuery: {
page: 1,
i_really_want_page_size: 10,
search: undefined,
is_deleted: false,
sort: undefined
},
typeOption: [
{ text: '钉钉', value: 'dingtalk' },
{ text: '阿里云短信', value: 'aliyun-sms', disabled: true }
],
temp: {
name: undefined,
jinja_template: undefined,
at: undefined,
access_token: undefined,
secret: undefined,
type: undefined,
content: undefined
},
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '编辑',
create: '创建'
},
rules: {}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.listLoading = true
listAlertTemplate(this.listQuery).then((response) => {
this.list = response.data.results
this.total = response.data.count
this.listLoading = false
})
},
handleFilter() {
this.listQuery.page = 1
this.getList()
},
handleDelete(row, index) {
this.$confirm('此操作将删除该路由规则, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteAlertTemplate(row.id).then(() => {
this.$notify({
title: 'Success',
message: 'Delete Successfully',
type: 'success',
duration: 2000
})
this.list.splice(index, 1)
})
})
},
handleUpdate(row) {
this.temp = Object.assign({}, row)
this.dialogFormVisible = true
this.dialogStatus = 'update'
this.$nextTick(() => {
this.$refs.dataForm.clearValidate()
})
},
updateData() {
this.$refs.dataForm.validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
console.log(tempData)
updateAlertTemplate(this.temp.id, tempData).then((response) => {
const index = this.list.findIndex((v) => v.id === this.temp.id)
this.list.splice(index, 1, response.data)
this.dialogFormVisible = false
this.$notify({
title: 'Success',
message: 'Update Successfully',
type: 'success',
duration: 2000
})
})
}
})
},
checkTemplate() {
this.$refs.dataForm.validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
checkAlertTemplate(tempData).then((response) => {
const { errcode, errmsg } = response.data
if (errcode === 0) {
this.$notify({
title: 'Success',
message: 'Successfully',
type: 'success',
duration: 2000
})
} else {
this.$notify({
title: 'ERROR',
message: errmsg,
type: 'error',
duration: 2000
})
}
})
}
})
},
handleCreate(row) {
this.temp = Object.assign({}, row)
this.dialogFormVisible = true
this.dialogStatus = 'create'
this.$nextTick(() => {
this.$refs.dataForm.clearValidate()
})
},
createData() {
this.$refs.dataForm.validate((valid) => {
if (valid) {
createAlertTemplate(this.temp).then((response) => {
this.list.unshift(response.data)
this.dialogFormVisible = false
this.$notify({
title: 'Success',
message: 'Created Successfully',
type: 'success',
duration: 2000
})
})
}
})
}
}
}
</script>

<style scoped>
.table-expand label {
width: 90px;
color: #99a9bf;
}

@media screen and (max-width: 500px) {
.el-dialog__wrapper .el-dialog {
width: 300px !important;
.el-dialog__body{
padding: 10px 20px!important;
.el-form-item__label{
width: 68px!important;
}
.el-select,.el-input{
width: 180px!important;
}
}
}
}
</style>
  1. 当前告警组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<template>
<div class="app-container">
<el-table
v-loading="listLoading"
:data="tableData"
stripe
border
fit
highlight-current-row
style="width: 100%"
element-loading-text="加载中"
>
<el-table-column align="center" label="Summary" width="100">
<template v-slot="{ row }">
<span v-if="row.annotations.summary">
{{ row.annotations.summary }}
</span>
</template>
</el-table-column>
<el-table-column align="center" label="description">
<template v-slot="{ row }">
<span v-if="row.annotations.description">
{{ row.annotations.description }}
</span>
</template>
</el-table-column>
<el-table-column align="center" label="state" width="70">
<template v-slot="{ row }">
<span v-if="row.state === 'firing'">
<el-tag effect="dark" type="danger">触发</el-tag>
</span>
<span v-if="row.state === 'pending'">
<el-tag effect="dark" type="warning">等待</el-tag>
</span>
</template>
</el-table-column>

</el-table>
<br>
<br>
<div class="block">
<el-pagination
:page-size="pageNation.pageSize"
:current-page="pageNation.currentPage"
:page-sizes="pageNation.pageSizes"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>

</div>

</template>

<script>
import { listAlerts } from '@/api/alerts_list'

export default {
name: 'Alerts',
data() {
return {
allData: [],
total: 0,
listLoading: false,
tableData: [],
pageNation: {
currentPage: 1,
pageSize: 30,
pageSizes: [20, 30, 50, 100]
}
}
},
created() {
this.init_data()
},
methods: {
init_data() {
this.listLoading = true
listAlerts().then((response) => {
this.allData = response.data
this.total = response.data.length
this.tableData = this.allData.slice(
(this.pageNation.currentPage - 1) * this.pageNation.pageSize,
this.pageNation.currentPage * this.pageNation.pageSize
)
this.listLoading = false
})
},
getTabelData() {
const data = JSON.parse(JSON.stringify(this.allData))
this.tableData = data.splice(
(this.pageNation.currentPage - 1) * this.pageNation.pageSize,
this.pageNation.pageSize
)
this.listLoading = false
},
handleSizeChange(val) {
this.pageNation.pageSize = val
this.getTabelData()
},
handleCurrentChange(val) {
this.pageNation.currentPage = val
this.getTabelData()
}
}
}
</script>

<style scoped>
.table-expand label {
width: 90px;
color: #99a9bf;
}
</style>