Django实现web端tailf日志文件功能及实例详解


Posted in Python onJuly 28, 2019

这是Django Channels系列文章的第二篇,以web端实现tailf的案例讲解Channels的具体使用以及跟Celery的结合

通过上一篇 《Django使用Channels实现WebSocket--上篇》 的学习应该对Channels的各种概念有了清晰的认知,可以顺利的将Channels框架集成到自己的Django项目中实现WebSocket了,本篇文章将以一个Channels+Celery实现web端tailf功能的例子更加深入的介绍Channels

先说下我们要实现的目标:所有登录的用户可以查看tailf日志页面,在页面上能够选择日志文件进行监听,多个页面终端同时监听任何日志都互不影响,页面同时提供终止监听的按钮能够终止前端的输出以及后台对日志文件的读取

最终实现的结果见下图

Django实现web端tailf日志文件功能及实例详解

接着我们来看下具体的实现过程

技术实现

所有代码均基于以下软件版本:

  • python==3.6.3
  • django==2.2
  • channels==2.1.7
  • celery==4.3.0

celery4在windows下支持不完善,所以请 在linux下运行 测试

日志数据定义

我们只希望用户能够查询固定的几个日志文件,就不是用数据库仅借助settings.py文件里写全局变量来实现数据存储

在settings.py里添加一个叫 TAILF 的变量,类型为字典,key标识文件的编号,value标识文件的路径

TAILF = {
 1: '/ops/coffee/error.log',
 2: '/ops/coffee/access.log',
}

基础Web页面搭建

假设你已经创建好了一个叫tailf的app,并添加到了settings.py的INSTALLED_APPS中,app的目录结构大概如下

tailf
 - migrations
 - __init__.py
 - __init__.py
 - admin.py
 - apps.py
 - models.py
 - tests.py
 - views.py

依然先构建一个标准的Django页面,相关代码如下

url:

from django.urls import path
from django.contrib.auth.views import LoginView,LogoutView
from tailf.views import tailf
urlpatterns = [
 path('tailf', tailf, name='tailf-url'),
 path('login', LoginView.as_view(template_name='login.html'), name='login-url'),
 path('logout', LogoutView.as_view(template_name='login.html'), name='logout-url'),
]

因为我们规定只有通过登录的用户才能查看日志,所以引入Django自带的LoginView,logoutView帮助我们快速构建Login,Logout功能

指定了登录模板使用 login.html ,它就是一个标准的登录页面,post传入username和password两个参数即可,不贴代码了

view:

from django.conf import settings
from django.shortcuts import render
from django.contrib.auth.decorators import login_required

# Create your views here.
@login_required(login_url='/login')
def tailf(request):
 logDict = settings.TAILF
 return render(request, 'tailf/index.html', {"logDict": logDict})

引入了 login_required 装饰器,来判断用户是否登录,未登录就给跳到 /login 登录页面

logDict去setting里取我们定义好的 TAILF 字典赋值,并传递给前端

template:

{% extends "base.html" %}

{% block content %}
<div class="col-sm-8">
 <select class="form-control" id="file">
 <option value="">选择要监听的日志</option>
 {% for k,v in logDict.items %}
 <option value="{{ k }}">{{ v }}</option>
 {% endfor %}
 </select>
</div>
<div class="col-sm-2">
 <input class="btn btn-success btn-block" type="button" onclick="connect()" value="开始监听"/><br/>
</div>
<div class="col-sm-2">
 <input class="btn btn-warning btn-block" type="button" onclick="goclose()" value="终止监听"/><br/>
</div>
<div class="col-sm-12">
 <textarea class="form-control" id="chat-log" disabled rows="20"></textarea>
</div>
{% endblock %}

前端拿到 TAILF 后通过循环的方式填充到select选择框下,因为数据是字典格式,使用 logDict.items 的方式可以循环出字典的key和value

这样一个日志监听页面就完成了,但还无法实现日志的监听,继续往下

集成Channels实现WebSocket

日志监听功能主要的设计思路就是页面跟后端服务器建立websocket长连接,后端通过celery异步执行while循环不断的读取日志文件然后发送到websocket的channel里,实现页面上的实时显示

接着我们来集成channels

先添加routing路由,直接修改 webapp/routing.py

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path, re_path
from chat.consumers import ChatConsumer
from tailf.consumers import TailfConsumer
application = ProtocolTypeRouter({
 'websocket': AuthMiddlewareStack(
 URLRouter([
 path('ws/chat/', ChatConsumer),
 re_path(r'^ws/tailf/(?P<id>\d+)/$', TailfConsumer),
 ])
 )
})

直接将路由信息写入到了 URLRouter 里,注意路由信息的外层多了一个list,区别于上一篇中介绍的写路由文件路径的方式

页面需要将监听的日志文件传递给后端,我们使用routing正则 P<id>\d+ 传文件ID给后端程序,后端程序拿到ID之后根据settings中指定的 TAILF 解析出日志路径

routing的写法跟Django中的url写法完全一致,使用 re_path 匹配正则routing路由

添加consumer在 tailf/consumers.py 文件中

import json
from channels.generic.websocket import WebsocketConsumer
from tailf.tasks import tailf
class TailfConsumer(WebsocketConsumer):
 def connect(self):
 self.file_id = self.scope["url_route"]["kwargs"]["id"]
 self.result = tailf.delay(self.file_id, self.channel_name)
 print('connect:', self.channel_name, self.result.id)
 self.accept()
 def disconnect(self, close_code):
 # 中止执行中的Task
 self.result.revoke(terminate=True)
 print('disconnect:', self.file_id, self.channel_name)
 def send_message(self, event):
 self.send(text_data=json.dumps({
 "message": event["message"]
 }))

这里使用Channels的单通道模式,每一个新连接都会启用一个新的channel,彼此互不影响,可以随意终止任何一个监听日志的请求

connect

我们知道 self.scope 类似于Django中的request,记录了丰富的请求信息,通过 self.scope["url_route"]["kwargs"]["id"] 取出routing中正则匹配的日志ID

然后将 id 和 channel_name 传递给celery的任务函数tailf,tailf根据 id 取到日志文件的路径,然后循环文件,将新内容根据 channel_name 写入对应channel

disconnect

当websocket连接断开的时候我们需要终止Celery的Task执行,以清除celery的资源占用

终止Celery任务使用到 revoke 指令,采用如下代码来实现

self.result.revoke(terminate=True)

注意 self.result 是一个result对象,而非id

参数 terminate=True 的意思是是否立即终止Task,为True时无论Task是否正在执行都立即终止,为False(默认)时需要等待Task运行结束之后才会终止,我们使用了While循环不设置为True就永远不会终止了

终止Celery任务的另外一种方法是:

from webapp.celery import app
app.control.revoke(result.id, terminate=True)
send_message

方便我们通过Django的view或者Celery的task调用给channel发送消息,官方也比较推荐这种方式

使用Celery异步循环读取日志

上边已经集成了Channels实现了WebSocket,但connect函数中的celery任务 tailf 还没有实现,下边来实现它

关于Celery的详细内容可以看这篇文章: 《Django配置Celery执行异步任务和定时任务》 ,本文就不介绍集成使用以及细节原理,只讲一下任务task

task实现代码如下:

from __future__ import absolute_import
from celery import shared_task
import time
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.conf import settings
@shared_task
def tailf(id, channel_name):
 channel_layer = get_channel_layer()
 filename = settings.TAILF[int(id)]
 try:
 with open(filename) as f:
 f.seek(0, 2)
 while True:
 line = f.readline()
 if line:
  print(channel_name, line)
  async_to_sync(channel_layer.send)(
  channel_name,
  {
  "type": "send.message",
  "message": "微信公众号【运维咖啡吧】原创 版权所有 " + str(line)
  }
  )
 else:
  time.sleep(0.5)
 except Exception as e:
 print(e)

这里边主要涉及到Channels中另一个非常重要的点: 从Channels的外部发送消息给Channel

其实 上篇文章 中检查通道层是否能够正常工作的时候使用的方法就是从外部给Channel通道发消息的示例,本文的具体代码如下

async_to_sync(channel_layer.send)(
 channel_name,
 {
 "type": "send.message",
 "message": "微信公众号【运维咖啡吧】原创 版权所有 " + str(line)
 }
)

channel_name对应于传递给这个任务的channel_name,发送消息给这个名字的channel

type对应于我们Channels的TailfConsumer类中的 send_message 方法,将方法中的 _ 换成 . 即可

message就是要发送给这个channel的具体信息

上边是发送给单Channel的情况,如果是需要发送到Group的话需要使用如下代码

async_to_sync(channel_layer.group_send)(
 group_name,
 {
 'type': 'chat.message',
 'message': '欢迎关注公众号【运维咖啡吧】'
 }
)

只需要将发送单channel的 send 改为 group_send , channel_name 改为 group_name 即可

需要特别注意的是: 使用了channel layer之后一定要通过async_to_sync来异步执行

页面添加WebSocket支持

后端功能都已经完成,我们最后需要添加前端页面支持WebSocket

function connect() {
 if ( $('#file').val() ) {
 window.chatSocket = new WebSocket(
 'ws://' + window.location.host + '/ws/tailf/' + $('#file').val() + '/');

 chatSocket.onmessage = function(e) {
 var data = JSON.parse(e.data);
 var message = data['message'];
 document.querySelector('#chat-log').value += (message);
 // 跳转到页面底部
 $('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
 };

 chatSocket.onerror = function(e) {
 toastr.error('服务端连接异常!')
 };

 chatSocket.onclose = function(e) {
 toastr.error('websocket已关闭!')
 };
 } else {
 toastr.warning('请选择要监听的日志文件')
 }
 }

上一篇文章 中有详细介绍过websocket的消息类型,这里不多介绍了

至此我们一个日志监听页面完成了,包含了完整的监听功能,但还无法终止,接着看下面的内容

Web页面主动断开WebSocket

web页面上“终止监听”按钮的主要逻辑就是触发WebSocket的onclose方法,从而可以触发Channels后端consumer的 disconnect 方法,进而终止Celery的循环读取日志任务

前端页面通过 .close() 可以直接触发WebSocket关闭,当然你如果直接关掉页面的话也会触发WebSocket的onclose消息,所以不用担心Celery任务无法结束的问题

function goclose() {
 console.log(window.chatSocket);

 window.chatSocket.close();
 window.chatSocket.onclose = function(e) {
 toastr.success('已终止日志监听!')
 };
 }

至此我们包含完善功能的Tailf日志监听、终止页面就全部完成了

总结

以上所述是小编给大家介绍的Django实现web端tailf日志文件功能及实例详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Python 相关文章推荐
状态机的概念和在Python下使用状态机的教程
Apr 11 Python
Python实现可设置持续运行时间、线程数及时间间隔的多线程异步post请求功能
Jan 11 Python
解决pandas无法在pycharm中使用plot()方法显示图像的问题
May 24 Python
Python实现的生产者、消费者问题完整实例
May 30 Python
Python爬虫的两套解析方法和四种爬虫实现过程
Jul 20 Python
Python实现多线程的两种方式分析
Aug 29 Python
对Python 内建函数和保留字详解
Oct 15 Python
Python requests.post方法中data与json参数区别详解
Apr 30 Python
python如何解析复杂sql,实现数据库和表的提取的实例剖析
May 15 Python
python新手学习使用库
Jun 11 Python
Python使用struct处理二进制(pack和unpack用法)
Nov 12 Python
pycharm 2020.2.4 pip install Flask 报错 Error:Non-zero exit code的问题
Dec 04 Python
Python assert语句的简单使用示例
Jul 28 #Python
对Django中static(静态)文件详解以及{% static %}标签的使用方法
Jul 28 #Python
解决Django Static内容不能加载显示的问题
Jul 28 #Python
基于Django静态资源部署404的解决方法
Jul 28 #Python
Python占用的内存优化教程
Jul 28 #Python
解决Django加载静态资源失败的问题
Jul 28 #Python
django之静态文件 django 2.0 在网页中显示图片的例子
Jul 28 #Python
You might like
详解WordPress中简码格式标签编写的基本方法
2015/12/22 PHP
PHP上传Excel文件导入数据到MySQL数据库示例
2016/10/25 PHP
PHP聊天室简单实现方法详解
2018/12/08 PHP
基于jquery点击自以外任意处,关闭自身的代码
2012/02/10 Javascript
一样的table?不一样的table(可编辑状态table)
2012/09/19 Javascript
js 实现菜单上下显示附效果图
2013/11/21 Javascript
javascript中createElement的两种创建方式
2015/05/14 Javascript
简述Jquery与DOM对象
2015/07/10 Javascript
jQuery三级下拉列表导航菜单代码分享
2020/04/15 Javascript
使用CoffeeScrip优美方式编写javascript代码
2015/10/28 Javascript
jQuery基础_入门必看知识点
2016/07/04 Javascript
Bootstrap 3 进度条的实现
2017/02/22 Javascript
windows下vue-cli导入bootstrap样式
2017/04/25 Javascript
详解vue嵌套路由-query传递参数
2017/05/23 Javascript
对于Javascript 执行上下文的全面了解
2017/09/05 Javascript
JS实现简易换图时钟功能分析
2018/01/04 Javascript
小程序实现列表删除功能
2018/10/30 Javascript
JavaScript数据结构之栈实例用法
2019/01/18 Javascript
vue项目前端错误收集之sentry教程详解
2019/05/27 Javascript
layui的布局和表格的渲染以及动态生成表格的方法
2019/09/18 Javascript
微信小程序引入VANT组件的方法步骤
2019/09/19 Javascript
js中复选框的取值及赋值示例详解
2020/10/18 Javascript
JavaScript实现网页跨年倒计时
2020/12/02 Javascript
Python使用os模块和fileinput模块来操作文件目录
2016/01/19 Python
利用numpy实现一、二维数组的拼接简单代码示例
2017/12/15 Python
python2.7实现FTP文件下载功能
2018/04/15 Python
Python实现的堆排序算法示例
2018/04/29 Python
TensorFlow2.0:张量的合并与分割实例
2020/01/19 Python
python 日志 logging模块详细解析
2020/03/31 Python
The North Face北面英国官网:美国著名户外品牌
2017/12/13 全球购物
英国领先的杂志订阅网站:Magazine.co.uk
2018/01/25 全球购物
学生上课看漫画的检讨书
2014/09/26 职场文书
党员作风建设整改方案
2014/10/27 职场文书
2014年出纳工作总结与计划
2014/12/09 职场文书
python+pytest接口自动化之token关联登录的实现
2022/04/06 Python
Fluentd搭建日志收集服务
2022/09/23 Servers