背景
监控系统的一般套路:采集->存储->展示->告警。
监控系统对于大数据平台的重要性不言而喻,一般是对大数据整个架构、各个数据的输入输出流、中间件的稳定性、数据的准确性、资源的使用情况、任务的执行情况进行监控。一般的监控告警通过采集告警日志、错误数据、关键词匹配等获取错误的数据进行实时展现并告警。
常见的监控系统以Grafana为基础,主要功能是将收集存储的数据按照不同维度、不同应用、不同用户进行配置化的展示;为了保证数据安全,每个团队只能看到自己的应用数据。同时对不同维度的数据,可以进行报警配置,根据最常用的报警方式,提供了钉钉报警、邮件报警、webhook报警三种方式。
不过最近在使用Flink的时候有一个业务场景,需要对历史数据进行监控,方便查看各个实时任务的表是否有数据产生。所以提供一个python脚本版的监控各个业务表的数据,并做钉钉告警的功能。
介绍
在做实时数据开发过程中,由于对接了不同的业务方,起了多个实时任务的程序,而数据的监控在运维那边,但运维同学只有针对整个集群的监控,对单个作业的监控还没建立起来,所以会初选一些实时任务在集群上runing的状态,但是对Kafka的消费却丢失,而Kafka目前只保留7天的数据,一旦数据丢失,需要通过离线任务去校验,会非常的耗时。
所以在这个背景上,单独做了针对自己输出的业务报表数据的监控,每天输出一些数据产生异常的表,并钉钉告警,方便快速处理。
整体的架构图如下:
如果业务没有离线校验的情况下,如何去监控数据表是否产生。
例如以钉钉告警为例:
1、建立钉钉机器人
钉钉开发文档ding-doc.dingtalk.com
2、完成安全设置后,复制出机器人的Webhook地址,可用于向这个群发送消息,格式如下:
https://oapi.dingtalk.com/robot/send?access_token=XXXXXX
同时指定全设置,一般选择关键词,例如:监控报警等,这个机器人所发送的消息,必须包含监控报警 这个词,才能发送成功。建立成功后显示:
3、编写监控数据库表的脚本:
# coding=utf-8
import datetime
import sys
import os
import requests
import json
import pymysql
class test_monitor():
def __init__(self):
self.database='warehouse'
self.host='localhost'
self.username='test'
self.password='123456'
self.table_list = [
{
"table_name": "ods_tgc_scu_index_online",
"ds": "date_create"
},
{
"table_name": "mid_jx_order_detail_online_result",
"ds": "created_at"
}
]
def get_data(self,table_name,ds_time):
try:
db = pymysql.connect(self.host, self.username, self.password, self.database, charset='utf8')
cursor = db.cursor()
yesterday = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime("%Y-%m-%d")
today = datetime.datetime.now().strftime("%Y-%m-%d")
sql = 'select count(1) from '+ table_name + ' where ' + ds_time + ' >= %s and '+ ds_time + ' < %s'
cursor.execute(sql,(yesterday,today))
data = cursor.fetchone()
if(data[0] > 0):
num = data[0]
return num
else:
num = 0
return num
cursor.close()
db.close()
except pymysql.InternalError as error:
code, message = error.args
print(">>>>>>>>>>>>>", code, message)
return -1
def push_data(self,table_name,result):
day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime("%Y-%m-%d")
db = pymysql.connect(self.host, self.username, self.password, self.database, charset='utf8')
cursor = db.cursor()
sql = 'INSERT INTO souche_enable_market_monitor (datab,table_name,num,ds) value (%s,%s,%s,%s)'
cursor.execute(sql,(self.database,table_name,result,day))
db.commit()
cursor.close()
db.close()
def run(self):
def dingmessage(self):
webhook = "https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXX"
header = {
"Content-Type": "application/json",
"Charset": "UTF-8"
}
message = {
"msgtype": "text",
"text": {
"content":self
},
"at": {
"atMobiles": [ #此处为需要@什么人。填写具体用户
"此处为需要@什么人。填写具体用户",
],
"isAtAll": True #此处为是否@所有人 True 所有人 False 无需所有人
}
}
message_json = json.dumps(message)
info = requests.post(url=webhook,data=message_json,headers=header)
print('发送成功')
print(info.json())
day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime("%Y-%m-%d")
for table in self.table_list:
table_name = table['table_name']
ds_time = table['ds']
result = self.get_data(table_name,ds_time)
self.push_data(table_name,result)
dingmessage('监控报警:n'+day+'n'+table_name+"数据量为:"+str(result))
if __name__ == '__main__':
test_monitor().run()
4、任务结果和监控
这样就可以每天看到昨天的数据是否产生,也可以设置阀值,将没达到阀值的表输出告警,然后方便去排查原因和恢复。
优化
上面的脚本是需要将配置文件写到脚本里面的,如果设计到的业务比较多,那么需要很多人同时修改这个脚本,没法做到数据安全的问题,所以下一步准备将这个配置文件生成一张表,每个人可以通过数据库insert的操作去添加自己需要监控的表。而且除了钉钉告警还可以发邮件。
例如:
Mysql数据条数的检测
- 目的:每天早上检查配置表中各条记录是否大于等于阈值,每天一条的,阈值写1即可。
- 结果:将不超过阀值的数据钉钉告警
1、表结构设计
CREATE TABLE `warehouse.dj_rpt_check_conf` (
`db` varchar(8) NOT NULL COMMENT '数据库别名,例如bi,online,warehouse结果库)' ,
`tbl` varchar(64) NOT NULL COMMENT '表名',
`condition` varchar(256) NOT NULL COMMENT '筛选条件',
`threshold` bigint(20) NOT NULL DEFAULT 0 COMMENT '阈值',
`owner` varchar(16) NOT NULL default 'nobody' COMMENT '负责人:每个人自己固定用一个名字',
`ptype` varchar(8) NOT NULL COMMENT '检查周期,例如:d(天),w(周,周一),m(月,1号)',
unique index tbl_db (tbl,db)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
插入数据:
insert into dj_rpt_check_conf values ('bi','dj_share_category_di','stat_date="${ds}"',20,'dy','d');
2、监控脚本
# coding: utf-8
from djtool import *
import pandas as pd
import sqlalchemy as sq
from sqlalchemy import exc
import sys
import requests
import json
import copy
owner_mobile={'gw':'123456789'}
def check_table(conn,table,condition,threshold):
try:
cursor = conn.cursor()
check_sql= 'select count(1) from ' + table + ' where ' + condition
cursor.execute(check_sql)
data = cursor.fetchone()
if(data[0]<threshold):
return data[0]
else:
return None
except pymysql.InternalError as error:
code, message = error.args
print(">>>>>>>>>>>>>", code, message)
return -1
ds= sys.argv[1]
check_conf = pd.read_sql_table('dj_rpt_check_conf',get_sqlalchemy_conn('mysql','bg'))
check_conf['condition'] = check_conf.condition.str.replace('${ds}',ds)
check_conf['real_cnt'] = 0
check_conf['failed'] = 0
for db in check_conf.db.unique():
db_conn=get_pymysql_conn("mysql_"+db)
for index,row in check_conf[(check_conf.db==db) & (check_conf.ptype=='d')].iterrows():
real_cnt = check_table(db_conn,row['tbl'],row['condition'],row['threshold'])
if(real_cnt is not None):
check_conf.loc[index,'real_cnt']=real_cnt
check_conf.loc[index,'failed']=1
mail_text = '''
配置表:`warehouse.dj_rpt_check_conf`
'''
fail_conf = check_conf[(check_conf.failed==1)]
if(fail_conf.shape[0]>0):
send_mail(['data@idongjia.cn'],[],ds+'--BI任务失败列表',mail_text + fail_conf.to_html())
else:
send_mail(['data@idongjia.cn'],[],ds+'--已经加入监控的BI任务完成:)',mail_text)
headers = {'Content-Type': 'application/json'}
ding_url = 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxx'
msg={
"msgtype": "markdown",
"markdown": {"title":"BI任务失败了:"+ds,
"text":"#### BI任务失败了:"+ds+" n @mobile 失败任务:n- fail_task "
},
"at": {
"atMobiles": [
"88888"
]
}
}
for owner in fail_conf.owner.unique():
tmp_msg = copy.deepcopy(msg)
tmp_msg['at']['atMobiles']=[owner_mobile[owner]]
tmp_msg['markdown']['text']=tmp_msg['markdown']['text'].replace('mobile',owner_mobile[owner])
tmp_msg['markdown']['text']=tmp_msg['markdown']['text'].replace('fail_task',"n- ".join(fail_conf[(fail_conf.owner==owner)].tbl.values.tolist()))
requests.post(ding_url, headers=headers,data=json.dumps(tmp_msg))
3、处理钉钉发邮件,可以通过自定义的方式发送邮件
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import smtplib
import sys
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr
from sys import argv
def main():
sender = 'test'
receivers = ['gaowei@test.cn']
password = 'xxxxxxxx'
message = MIMEMultipart()
message['From'] = formataddr(["数据组",sender])
message['To'] = formataddr(["数据组成员",receivers])
subject = 'rest'
message['Subject'] = Header(subject, 'utf-8')
message.attach(MIMEText(sys.argv[1]+'数据见附件n', 'plain', 'utf-8'))
att1 = MIMEText(open("aa.csv", 'rb').read(), 'base64', 'utf-8')
att1["Content-Type"] = 'application/octet-stream'
att1["Content-Disposition"] = 'attachment; '+'filename='+sys.argv[1]+'.csv'
message.attach(att1)
try:
server=smtplib.SMTP_SSL("smtp.exmail.qq.com", 465)
server.login(sender, password)
server.sendmail(sender,receivers,message.as_string())
print "邮件发送成功"
except smtplib.SMTPException:
print "Error: 无法发送邮件"
if __name__ == '__main__':
main()
不过这个执行过程,需要将要发的文本先下载到本地,然后才能发送,所以一般的执行脚本如下:
#!/usr/bin/env bash
export JAVA_HOME=/opt/jdk1.8.0_121
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=$PATH:${JAVA_HOME}/bin:{JRE_HOME}/bin:$PATH
export PATH=$PATH:/opt/mysql/bin
dateStrDay=$1
if [ -z "$1" ] ; then
dateStrDay=`date +%Y-%m-%d`
fi
echo $dateStrDay
dateStr=`date +%Y-%m-%d-%M-%S`
hadoop fs -rm -r /tmp/gaowei/test
cd /opt/task/gaowei/warehouse/test
rm *.csv
spark2-submit --class cn.idongjia.data.auction.AuctionOrder --master yarn --deploy-mode cluster /opt/task/gaowei/warehouse/test/datawarehouse_2.11-1.0.jar
hadoop fs -getmerge /tmp/gaowei/test/* /opt/task/gaowei/warehouse/test/bb.csv
sed '1i用户id,拍卖订单数,跑单数,异常订单数,是否禁言禁拍(0表示否,1表示是),是否在白名单(0表示否,1表示是),是否屏蔽(0表示否,1表示是,时间)' /opt/task/gaowei/warehouse/test/bb.csv > /opt/task/gaowei/warehouse/test/aa.csv
python Email.py ${dateStrDay}
扩展
除了python发邮件,Scala和Java也可以直接发邮件,代码如下:
package spark_tmp.utils
import java.io.File
import com.typesafe.config.ConfigFactory
import org.apache.spark.rdd.RDD
import play.api.libs.mailer._
/** *
*
* @autor gaowei
* @Date 2020-03-17 17:31
*/
object TaskSendMail {
/**
* 定义一个发邮件的人
* @param host STMP服务地址
* @param port STMP服务端口号
* @param user STMP服务用户邮箱
* @param password STMP服务邮箱密码
* @param timeout setSocketTomeout 默认: 60s
* @param connectionTimeout setSocketConnectionTimeout 默认:60s
* @return 返回一个可以发邮件的用户
*/
def createMailer(host:String, port: Int, user: String, password: String, timeout:Int = 10000, connectionTimeout:Int = 10000):SMTPMailer ={
// STMP服务SMTPConfiguration
val configuration = new SMTPConfiguration(
host, port, false, false, false,
Option(user), Option(password), false, timeout = Option(timeout),
connectionTimeout = Option(connectionTimeout), ConfigFactory.empty(), false
)
val mailer: SMTPMailer = new SMTPMailer(configuration)
mailer
}
/**
* 生成一封邮件
* @param subject 邮件主题
* @param from 邮件发送地址
* @param to 邮件接收地址
* @param bodyText 邮件内容
* @param bodyHtml 邮件的超文本内容
* @param charset 字符编码 默认: utf-8
* @param attachments 邮件的附件
* @return 一封邮件
*/
def createEmail(subject:String, from:String, to:Seq[String], bodyText:String = "ok", bodyHtml:String = "", charset:String = "utf-8", attachments:Seq[Attachment] = Seq.empty): Email = {
val email = Email(subject, from, to,
bodyText = Option[String](bodyText), bodyHtml = Option[String](bodyHtml),
charset= Option[String](charset),attachments = attachments
)
email
}
/**
* 生成一个附件
* @param name 附件的名字
* @param fileStr 以本地文件为附件相关参数
* @param rdd 以hdfs文件或rdd或df为附件相关参数
* @return
*/
def createAttachments(name: String, fileStr: String = "", rdd:RDD[String] = null): Attachment = {
var attachment: Attachment = null
if(fileStr.contains(":")){
val file: File = new File(fileStr)
attachment = AttachmentFile(name, file)
}else{
val data: Array[Byte] = rdd.collect().mkString("n").getBytes()
// 根据文件类型选择MimeTypes对应的值
val mimetype = "text/plain"
attachment = AttachmentData(name, data, mimetype)
}
attachment
}
/**
* 主要针对日常简单结果的快速发送
* @param subject 邮件主题名字
* @param toStr 邮件的接收人,多名以,分割
* @param bodyText 邮件的内容
* @return 用户设备 <510109769.1.1561635225728@RAN>
*/
def dailyEmail(subject:String, toStr:String, bodyText: String):String={
val to = toStr.split(",").toList
// 阿里云企业 邮箱
val host = "smtp.189.cn"
val port = 25
val user = "18968044961@189.cn"
val password = "xxxxxxxxxx"
val from = user
val mailer: SMTPMailer = TaskSendMail.createMailer(host, port, user, password)
val email: Email = TaskSendMail.createEmail(subject, from, to, bodyText = bodyText)
val userdev: String = mailer.send(email)
userdev
}
}