问题描述:
有个业务经常出问题,无奈只好在 源头就开始监控告警,在处理端,源头端,目标端全部做相应措施。我这里只演示源头端监控告警,监控细粒度直接干到日志级别
项目场景
1.我有6台机器:
主机名 | ip地址 |
test1 | 192.168.5.11 |
test2 | 192.168.5.12 |
test3 | 192.168.5.13 |
test4 | 192.168.5.14 |
test5 | 192.168.5.15 |
test6 | 192.168.5.16 |
2.我的程序会被调度组件调度到以上的任意一台机器上跑,且日志会打到这台机器的标准路径/data/test/log下
3.我的日志格式是这样的test_id.log格式,如下:
[root@test1 log] ls
test_12.log test_13.log test_15.log test_212.log test_312.log
4.第3步的id(主键自增1)存在数据库test_id表里,test_id表字段如下:
字段 | 数据类型 |
id | int |
name | varchar(20) |
实际数据如下:
id | name |
231 | test1流程 |
232 | test2流程 |
233 | test1流程 |
235 | test2流程 |
236 | test1流程 |
238 | test1流程 |
以test1流程为例,每次重启会生成新的id,比如上一次的id是235,重启后新生成的id就会增1,而且一个流程只能有一个现在时间点的id,大小小的都是历史id
mysql>select id from test where name='test1流程 ';
231 | test1流程 |
233 | test1流程 |
236 | test1流程 |
238 | test1流程 |
像这样,这个时候,test1流程的id就是238,231和233 236是他历史的id
5.调度组件将我的程序调度到哪台机器上去了,并没有存在数据库里(这里我们后面自己写函数去到指定目录查找)
6.现在的需求就是: 对test1流程现在的日志实时监控,并发现关键字error就告警,比如现在日志文件是:test_ 238.log,在哪台机器上不知道
函数分析:
算法思路:
1.要有告警的动作,邮件告警,那这里可以写一个函数
2.通过name找到这个id是多少
3.找到id后,通过ssh到结点指定目录查找,找到了,该结点就是日志文件所在结点
4.找到结点后,通过paramiko模块实现远程控制,打印远程机器的日志,或者监控远程日志
5.拿到日志得做出判断,什么时候开始告警
所有东西都根据算法来,算法不会就细致点拆分步骤,代码不熟练一点都不重要,不用纠结if,for,函数格式是如何的都是可以搬砖,拿来抄,能把东西做出来就ok
解决方案:
开始上才艺,根据算法,一步一步来,在主节点192.168.5.11上操作:
1.邮件告警函数编写:
def send_mail(body):
# 邮箱登录信息
sender = 'youruername@163.com'
password = 'yourpasswd'
# 收件人邮箱,也可以是前面的youruername@163.com,反正只要能收到就行
receiver = 'xxxx@qq.com'
# 邮件内容
subject = 'Python邮件示例'
#message = '这是一封通过Python发送的test邮件。'
# 构造邮件对象
msg = MIMEText(body, 'plain', 'utf-8')
msg['From'] = Header(sender)
msg['To'] = Header(receiver)
msg['Subject'] = Header(subject)
try:
# 实例化smtp对象,连接SMTP服务器
server = smtplib.SMTP('smtp.163.com', 25)
server.login(sender, password)
# 发送邮件,调用sendmail方法
server.sendmail(sender, receiver, msg.as_string())
print('日志异常,邮件发送成功')
except Exception as e:
print('业务异常,邮件发送失败:', str(e))
finally:
# 关闭连接
server.quit()
#send_mail('这是一个邮件测试')
编写完后将这个测试一下,是否能发送邮件成功
2.钉钉等告警媒介函数编写,我这里这是演示,不过大概的函数都差不多:
def send_message_to_group(url, message):
header = {
"Content-Type": "application/json"
}
data = {
"msg_type":"text",
"content": {
"text": f"{message}"
}
}
rsp = requests.post(url=url, headers=header, json=data)
print(rsp.text)
3.数据库里拿id并返回id号,数据库操作:
def get_id_info():
print("开始数据库操作,获取id")
# 打开数据库进行链接 host 主机名或ip user:用户名 password:密码 database:数据库名称
db = pymysql.connect(host="192.168.5.11", user="test_user", password="test_passwd", port=3306,database="test_db")
# 使用cursor方法创建一个游标对象
cursor = db.cursor()
# 使用 execute 方法执行sql 查询,因为每次只能拿到最大的那个(业务特点),所以倒序排序
cursor.execute("select id from test where name='test1流程' order by id desc limit 1;")
# 使用 fetchall 方法获取多条数据 单条数据用 fetchone()
data = cursor.fetchall()
# 输出数据
db.close()
# 关闭数据库
return data
4.找到id对应的日志文件对应机器的ip地址:
def find_id_node(id,hosts,path):
pkey = paramiko.RSAKey.from_private_key_file('/root/.ssh/id_rsa')
command = f'ls {path}|grep {id}'
found_host = None
for ip in hosts:
print(f'正在{ip}上找是否存在id号是{id}')
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=ip, port=22, username='root', pkey=pkey)
stdin,stdout,stderr = ssh.exec_command(command)
# 获取命令执行结果
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
# 判断文件是否存在
#print(output)
#如果error为空,output为真
if not error and output:
found_host = ip
break
# 关闭SSH连接
ssh.close()
if found_host:
print(f'id号是{id}的文件在主机 {found_host} 上找到')
logging.info(f'id号是{id}的文件在主机 {found_host} 上找到')
output = output.strip("\n")
return found_host,output
else:
print('文件未找到')
5.远程监控日志,使用paramiko模块get_transport方法:
def ssh_monitor_log(host,test_path,log_file):
print(host)
path = os.path.join(test_path,log_file)
pkey = paramiko.RSAKey.from_private_key_file('/root/.ssh/id_rsa')
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=host, port=22, username='root', pkey=pkey)
# 创建SSH会话
transport = ssh.get_transport()
channel = transport.open_session()
channel.exec_command(f"tail -f {path}")
try:
#print(stdout)
# 实时获取输出
while True:
if channel.recv_ready():
output = channel.recv(1024).decode('utf-8')
if output:
output = output.lower()
if 'error' in output:
index = output.index(error)
start_index = max(0, index - 10) # 关键字前10个字符的起始索引
end_index = min(len(output), index + len('error') + 50) # 关键字后50个字符的结束索引
err_message = output[start_index:end_index]
message = f"test1流程,在{host}机器上,日志文件是{log_file}有报错请检查: \n {err_message}"
send_message_to_group(url, message)
send_mail(err_message)
except Exception as e:
print('监控业务日志异常:', str(e))
logging.info('监控业务日志异常:', str(e))
message = f"监控业务日志异常:, {str(e)}"
send_message_to_group(url, message)
send_mail(message)
finally:
# 关闭SSH连接
channel.close()
transport.close()
ssh.close()
6.正文:
# 配置日志输出到文件
logging.basicConfig(filename='monitor_test.log', level=logging.INFO)
id = get_id_info()[0][0]
hostip = ['192.168.5.11','192.168.5.12','192.168.5.13','192.168.5.14','192.168.5.15','192.168.5.16']
test_path = '/data/test/log/'
#取函数返回值,结果是元组
id_host_and_logfile = find_id_node(id,hostip,test_path)
#ip地址赋值给id_host
id_host = id_host_and_logfile[0]
logging.info(f'id_host is {id_host}')
print(f'id_host is {id_host}')
#日志文件赋值给log_file
log_file = id_host_and_logfile[1]
#path = os.path.join(test_path,log_file)
#钉钉或者其他群组的私密信息
url = 'https://yourgroup/your_secret'
ssh_monitor_log(id_host,test_path,log_file)
7.导入模块:
import smtplib
import tailer
from email.mime.text import MIMEText
from email.header import Header
import requests
import pymysql
import paramiko
import os
import socket
import logging
8.其他:
如果想在192.168.5.11,实时打印日志,怎么改程序:
def ssh_monitor_log改一下,这里不演示截图了
transport = ssh.get_transport()
channel = transport.open_session()
channel.exec_command(f"tail -f {path}")
try:
#print(stdout)
# 实时获取输出
while True:
if channel.recv_ready():
output = channel.recv(1024).decode('utf-8')
#改成这样
if output:
print(output)