背景

监控系统的一般套路:采集->存储->展示->告警。

监控系统对于大数据平台的重要性不言而喻,一般是对大数据整个架构、各个数据的输入输出流、中间件的稳定性、数据的准确性、资源的使用情况、任务的执行情况进行监控。一般的监控告警通过采集告警日志、错误数据、关键词匹配等获取错误的数据进行实时展现并告警。

常见的监控系统以Grafana为基础,主要功能是将收集存储的数据按照不同维度、不同应用、不同用户进行配置化的展示;为了保证数据安全,每个团队只能看到自己的应用数据。同时对不同维度的数据,可以进行报警配置,根据最常用的报警方式,提供了钉钉报警、邮件报警、webhook报警三种方式。

不过最近在使用Flink的时候有一个业务场景,需要对历史数据进行监控,方便查看各个实时任务的表是否有数据产生。所以提供一个python脚本版的监控各个业务表的数据,并做钉钉告警的功能。

介绍

在做实时数据开发过程中,由于对接了不同的业务方,起了多个实时任务的程序,而数据的监控在运维那边,但运维同学只有针对整个集群的监控,对单个作业的监控还没建立起来,所以会初选一些实时任务在集群上runing的状态,但是对Kafka的消费却丢失,而Kafka目前只保留7天的数据,一旦数据丢失,需要通过离线任务去校验,会非常的耗时。

所以在这个背景上,单独做了针对自己输出的业务报表数据的监控,每天输出一些数据产生异常的表,并钉钉告警,方便快速处理。

整体的架构图如下:




zabbix钉钉报警 钉钉告警系统_mysql


如果业务没有离线校验的情况下,如何去监控数据表是否产生。


zabbix钉钉报警 钉钉告警系统_mysql_02


例如以钉钉告警为例:

1、建立钉钉机器人

钉钉开发文档ding-doc.dingtalk.com

2、完成安全设置后,复制出机器人的Webhook地址,可用于向这个群发送消息,格式如下:


https://oapi.dingtalk.com/robot/send?access_token=XXXXXX


同时指定全设置,一般选择关键词,例如:监控报警等,这个机器人所发送的消息,必须包含监控报警 这个词,才能发送成功。建立成功后显示:


zabbix钉钉报警 钉钉告警系统_zabbix钉钉报警_03


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、任务结果和监控


zabbix钉钉报警 钉钉告警系统_钉钉机器人关键词应答_04


zabbix钉钉报警 钉钉告警系统_数据_05


这样就可以每天看到昨天的数据是否产生,也可以设置阀值,将没达到阀值的表输出告警,然后方便去排查原因和恢复。

优化

上面的脚本是需要将配置文件写到脚本里面的,如果设计到的业务比较多,那么需要很多人同时修改这个脚本,没法做到数据安全的问题,所以下一步准备将这个配置文件生成一张表,每个人可以通过数据库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
  }
}