django+tornado实现实时查看远程日志的方法


Posted in Python onAugust 12, 2019

大致思路:

1.利用tornado提供的websocket功能与浏览器建立长连接,读取实时日志并输出到浏览器

2.写一个实时读取日志的脚本,利用saltstack远程执行,并把实时日志发往redis中。

3.tornado读取redis中的信息,发往浏览器。

此过程用到了redis的发布和订阅功能。

先看一下tornado中是如何处理的:

import os
import sys
import tornado.websocket
import tornado.web
import tornado.ioloop
import redis
import salt.client

from tornado import gen
from tornado.escape import to_unicode

from logs.utility import get_last_lines
from logs import settings


class SubWebSocket(tornado.websocket.WebSocketHandler):
 """
 此handler处理远程日志查看
 """
 def open(self, *args, **kwargs):
  print("opened")

 @gen.coroutine
 def on_message(self, message):
  # 主机名,要查看的日志路径,运行脚本的命令这些信息从浏览器传过来
  hostname, log_path, cmd = message.split("||")
  local = salt.client.LocalClient()
  r = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT,
        password=settings.REDIS_PASSWD, db=5)
  # 订阅频道,服务器和日志路径确定一个频道
  key = settings.LOG_KEY.format(server=hostname.strip(), log_path=log_path.strip())
  channel = r.pubsub()
  channel.subscribe(key)
  # 异步方式执行命令,远程运行脚本
  local.cmd_async(hostname, "cmd.run", [cmd])
  try:
   while True:
    data = channel.get_message()
    if not data:
     # 如果读取不到消息,间隔一定时间,避免无谓的CPU消耗
     yield gen.sleep(0.05)
     continue
    if data["type"] == "message":
     line = format_line(data["data"])
     self.write_message(line)
  except tornado.websocket.WebSocketClosedError:
   self.close()

 def on_close(self):
  global FLAG
  FLAG = False
  print("closed")


def format_line(line):
 line = to_unicode(line)
 if "INFO" in line:
  color = "#46A3FF"
 elif "WARN" in line:
  color = "#FFFF37"
 elif "ERROR" in line:
  color = "red"
 elif "CRITICAL" in line:
  color = "red"
 else:
  color = "#FFFFFF"

 return "<span style='color:{}'>{}</span>".format(color, line)


class EchoWebSocket(tornado.websocket.WebSocketHandler):
 def open(self):
  print("WebSocket opened")

 @gen.coroutine
 def on_message(self, message):
  log = message
  print "log file: ", log

  try:
   with open(log, 'r') as f:
    for line in get_last_lines(f):
     line1 = format_line(line)
     self.write_message(line1)
    while True:
     line = f.readline()
     if not line:
      yield gen.sleep(0.05)
      continue
     self.write_message(format_line(line.strip()))
  except tornado.websocket.WebSocketClosedError as e:
   print e
   self.close()

 # def check_origin(self, origin):
 #  print origin, self.request.headers.get("Host")
 #  # super(EchoWebSocket, self).check_origin()
 #  return True

 def on_close(self):
  print("WebSocket closed")


class Application(tornado.web.Application):
 def __init__(self):
  handlers = [
   (r'/log/', MainHandler), # 提供浏览页面,页面中的JS与服务器建立连接
   (r'/log/local', EchoWebSocket), # 处理本地日志实时查看,比较简单
   (r'/log/remote', SubWebSocket), # 处理远程日志实时查看,稍微复杂
  ]
  settings = {
   "debug": True,
   "template_path": os.path.join(os.path.dirname(__file__), "templates"),
   "static_path": os.path.join(os.path.dirname(__file__), "static"),
  }
  super(Application, self).__init__(handlers, **settings)


class MainHandler(tornado.web.RequestHandler):
 def get(self):
  # 要查看的日志路径
  log = self.get_argument("log", None)
  # hostname实际上是saltstack中这台机器对应的minion id
  hostname = self.get_argument("hostname", None)
  # 本地日志还是远程日志
  type = self.get_argument("type", "local")
  # 运行读取实时日志的脚本,参数比较多,后面会有
  cmd = self.get_argument("cmd", "")
  context = {
   "log": log,
   "hostname": hostname,
   "type": type,
   "cmd": cmd,
  }
  self.render("index.html", **context)

配置文件中主要记录了redis服务器的地址等信息

# encoding: utf-8

LOG_KEY = "logs:{server}:{log_path}"

LOG_NAME = "catalina.out"
TAIL_LINE_NUM = 20

REDIS_HOST = "127.0.0.1"
REDIS_PORT = "6379"
REDIS_PASSWD = None
REDIS_EXPIRE = 300

try:
 from local_settings import *
except ImportError:
 pass

index.html的内容如下:

<html>
<head>
<link href="{{ static_url('public/css/public.css') }}" rel="external nofollow" rel="stylesheet" />
<link href="{{ static_url('kylin/css/style.css') }}" rel="external nofollow" rel="stylesheet" />
</head>
<body style="background:#000000">
<div style="margin-left:10px;">
 <pre id="id-content">
 </pre>
 <div id="id-bottom"></div>
 <input type="hidden" id="id-log" value="{{ log }}" />
 <input type="hidden" id="id-type" value="{{ type }}" />
 <input type="hidden" id="id-hostname" value="{{ hostname }}" />
 <input type="hidden" id="id-cmd" value="{{ cmd }}" />
 <div class="btns btns_big">
  <button type="button" class="query_btn cancle" id="id-stop">Stop</button>
  <button type="button" class="query_btn commit" id="id-start">Start</button>
 </div>
</div>
<script type="text/javascript" src="{{ static_url('js/jquery-1.11.3.min.js') }}"></script>
<script type="text/javascript">
 var log_name = $("#id-log").val();
 var type = $("#id-type").val();
 var hostname = $("#id-hostname").val();
 var cmd = $("#id-cmd").val();
 // 初始化websocket对象
 var ws = new WebSocket("ws://{{ request.host }}/log/" + type);
 ws.onopen = function(){
  if (type === "local"){
   ws.send(log_name);
  } else {
   // 建立连接后把相关信息发往服务器,对应上面的SubWebSocket
   ws.send(hostname + "||" + log_name + "||" + cmd);
  }
 };
 var get_message = function(evt){
  $("#id-content").append(evt.data + "\n");
  document.getElementById("id-bottom").scrollIntoView()
 };
 ws.onmessage = get_message;
 // 两个按钮控制日志的输出,如果看到需要的日志信息,可以暂停日志的输出,
 // 之后可以继续启动日志的输出
 $("#id-stop").click(function(){
  ws.onmessage = function(){};
 })
 $("#id-start").click(function(){
  ws.onmessage = get_message;
 })
</script>
</body>
</html>

这个tornado仅仅是提供了实时日志的服务,实际项目使用的是django,django中要做的其实很简单,提供log_name,hostname,type,cmd等四个参数。

下面看一个实例:

class LogView(KylinView):
 # 实时读取日志的脚本,事先使用saltstack批量传到各台服务器上
 client_path = "/tmp/logtail.py"

 def get(self, request):
  minion_id = request.GET.get("minion_id")
  context = {
   "minion_id": minion_id,
   "tail_log_url": settings.TAIL_LOG_URL,
  }
  return render(request, "cmdb/log_view.html", context)

 def post(self, request):
  minion_id = request.POST.get("minion_id")
  log_path = request.POST.get("log_path")
  if not log_path:
   return JsonResponse({"success": False, "message": "请填写日志路径"})
  try:
   # 制定一开始读取的行数
   line_count = request.POST.get("line_count")
  except (TypeError, ValueError):
   return JsonResponse({"success": False, "message": "请输入正确的行数"})
  local = salt.client.LocalClient()
  # 确保saltstack能连通并且日志文件存在
  ret = local.cmd(minion_id, "file.file_exists", [log_path])
  if minion_id not in ret:
   return JsonResponse({"success": False, "message": "服务器无法连通"})
  if not ret[minion_id]:
   return JsonResponse({"success": False, "message": "日志文件不存在"})
  # 组成命令的各个参数,redis信息需要和tornado配置文件中的redis信息一致
  cmd = "{} {} {} {} {} {} {} {}".format(
   settings.PYTHON_BIN, self.client_path, minion_id, log_path, line_count, settings.REDIS_HOST,
   settings.REDIS_PORT, settings.REDIS_PASSWD)
  # settings.TAIL_LOG_URL是tornado中MainHandler对应的url,把其它几个
  # 参数组合成最终的URL,直接访问这个URL就可以在浏览器中实时读取日志了。
  url = "{}?type=remote&log={}&hostname={}&cmd={}".format(
   settings.TAIL_LOG_URL, log_path, minion_id, cmd)
  # 这一步的操作确保同一个日志文件只有一个脚本在读取,避免日志信息重复,这一步
  # 也很重要,必不可少
  local.cmd(minion_id, "cmd.run",
     ["kill `ps aux|grep logtail.py|grep %s|grep -v grep|awk '{print $2}'`" % (log_path,)])
  return JsonResponse({"success": True, "url": url})

下面来看看logtail.py的实现:

# encoding: utf-8
from __future__ import unicode_literals, division

import math
import time
import sys
import socket
import signal
import redis

FLAG = True


def get_last_lines(f, num=10):
 """读取文件的最后几行
 """
 size = 1000
 try:
  f.seek(-size, 2)
 except IOError: # 文件内容不足size
  f.seek(0)
  return f.readlines()[-num:]

 data = f.read()
 lines = data.splitlines()
 n = len(lines)
 while n < num:
  size *= int(math.ceil(num / n))
  try:
   f.seek(-size, 2)
  except IOError:
   f.seek(0)
   return f.readlines()[-num:]
  data = f.read()
  lines = data.splitlines()
  n = len(lines)

 return lines[-num:]


def process_line(r, channel, line):
 r.publish(channel, line.strip())


def sig_handler(signum, frame):
 global FLAG
 FLAG = False


# 收到退出信号后,以比较优雅的方式终止脚本
signal.signal(signal.SIGTERM, sig_handler)
# 为了避免日志输出过多,浏览器承受不住,设置5分钟后脚本自动停止
signal.signal(signal.SIGALRM, sig_handler)
signal.alarm(300)


def get_hostname():
 return socket.gethostname()


def force_str(s):
 if isinstance(s, unicode):
  s = s.encode("utf-8")
 return s


def tail():
 password = sys.argv[6]
 if password == "None":
  password = None
 r = redis.StrictRedis(host=sys.argv[4], port=sys.argv[5], password=password, db=5)
 log_path = sys.argv[2]
 line_count = int(sys.argv[3])
 # 往redis频道发送实时日志
 channel = "logs:{hostname}:{log_path}".format(hostname=sys.argv[1], log_path=log_path)

 with open(log_path, 'r') as f:
  last_lines = get_last_lines(f, line_count)
  for line in last_lines:
   process_line(r, channel, force_str(line))
  try:
   while FLAG: # 通过信号控制这个变量,实现优雅退出循环
    line = f.readline()
    if not line:
     time.sleep(0.05)
     continue
    process_line(r, channel, line)
  except KeyboardInterrupt:
   pass
 print("Exiting...")

if __name__ == "__main__":
 if len(sys.argv) < 6:
  print "Usage: %s minion_id log_path host port redis_pass"
  exit(1)

 tail()

到此为止,整个实时读取远程日志的流程就讲完了。

github: https://github.com/tuxinhang1989/logs

以上这篇django+tornado实现实时查看远程日志的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
跟老齐学Python之for循环语句
Oct 02 Python
Python3.2中的字符串函数学习总结
Apr 23 Python
python机器学习之决策树分类详解
Dec 20 Python
TensorFlow 模型载入方法汇总(小结)
Jun 19 Python
python实现下载pop3邮件保存到本地
Jun 19 Python
python3实现点餐系统
Jan 24 Python
Python字符串内置函数功能与用法总结
Apr 16 Python
一篇文章弄懂Python中的可迭代对象、迭代器和生成器
Aug 12 Python
Python Opencv提取图片中某种颜色组成的图形的方法
Sep 19 Python
使用Keras预训练模型ResNet50进行图像分类方式
May 23 Python
Python 利用argparse模块实现脚本命令行参数解析
Dec 28 Python
Python之基础函数案例详解
Aug 30 Python
Django结合ajax进行页面实时更新的例子
Aug 12 #Python
django fernet fields字段加密实践详解
Aug 12 #Python
利用pyecharts实现地图可视化的例子
Aug 12 #Python
django echarts饼图数据动态加载的实例
Aug 12 #Python
python scrapy爬虫代码及填坑
Aug 12 #Python
Python 中的 global 标识对变量作用域的影响
Aug 12 #Python
Python中pymysql 模块的使用详解
Aug 12 #Python
You might like
15种PHP Encoder的比较
2007/04/17 PHP
php中大括号作用介绍
2012/03/22 PHP
php地址引用(php地址引用的效率问题)
2012/03/23 PHP
PHP代码判断设备是手机还是平板电脑(两种方法)
2015/10/19 PHP
Javascript SHA-1:Secure Hash Algorithm
2006/12/20 Javascript
经常用到的JavasScript事件的翻译
2007/04/09 Javascript
javascript下查找父节点的简单方法
2007/08/13 Javascript
js 浮动层菜单收藏
2009/01/16 Javascript
AeroWindow 基于JQuery的弹出窗口插件
2011/06/27 Javascript
html中使用javascript调用本地程序(exe、doc等)实现代码
2013/04/26 Javascript
jQuery插件实现大图全屏图片相册
2015/03/14 Javascript
javascript实现随时变化着的背景颜色
2015/04/02 Javascript
jQuery横向擦除焦点图特效代码分享
2015/09/06 Javascript
利用jQuery和CSS将背景图片拉伸
2015/10/16 Javascript
javascript数据结构之双链表插入排序实例详解
2015/11/25 Javascript
AngularJS教程之环境设置
2016/08/16 Javascript
分分钟学会vue中vuex的应用(入门教程)
2017/09/14 Javascript
vue.js实例对象+组件树的详细介绍
2017/10/20 Javascript
egg.js的基本使用和调用数据库的方法示例
2019/05/18 Javascript
python中查看变量内存地址的方法
2015/05/05 Python
python二维列表一维列表的互相转换实例
2018/07/02 Python
pycharm远程开发项目的实现步骤
2019/01/20 Python
解决python3中的requests解析中文页面出现乱码问题
2019/04/19 Python
python3调用windows dos命令的例子
2019/08/14 Python
Python facenet进行人脸识别测试过程解析
2019/08/16 Python
Django--权限Permissions的例子
2019/08/28 Python
python模块导入的方法
2019/10/24 Python
详解Flask前后端分离项目案例
2020/07/24 Python
html5 拖拽及用 js 实现拖拽功能的示例代码
2020/10/23 HTML / CSS
全球最大的网上自行车商店:Chain Reaction Cycles
2016/12/02 全球购物
吃空饷专项整治方案
2014/10/27 职场文书
生日答谢词
2015/01/05 职场文书
2015年中学校长工作总结
2015/05/19 职场文书
区域销售大会开幕词
2016/03/04 职场文书
2019让人心动的商业计划书
2019/06/27 职场文书
Win11怎么解除儿童账号限制?Win11解除微软儿童账号限制方法
2022/07/07 数码科技