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 sys.path详细介绍
Oct 17 Python
Python列表(list)、字典(dict)、字符串(string)基本操作小结
Nov 28 Python
Python实现对一个函数应用多个装饰器的方法示例
Feb 09 Python
PyCharm安装第三方库如Requests的图文教程
May 18 Python
pycharm 主题theme设置调整仿sublime的方法
May 23 Python
windows 10 设定计划任务自动执行 python 脚本的方法
Sep 11 Python
Python input函数使用实例解析
Nov 22 Python
详解Python中pyautogui库的最全使用方法
Apr 01 Python
keras和tensorflow使用fit_generator 批次训练操作
Jul 03 Python
Python 使用office365邮箱的示例
Oct 29 Python
2021年最新用于图像处理的Python库总结
Jun 15 Python
python中__slots__节约内存的具体做法
Jul 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
PHP pathinfo()获得文件的路径、名称等信息说明
2011/09/13 PHP
详解PHP错误日志的获取方法
2015/07/20 PHP
thinkphp3.x中display方法及show方法的用法实例
2016/05/19 PHP
javascript mouseover、mouseout停止事件冒泡的解决方案
2009/04/07 Javascript
jQuery创建插件的代码分析
2011/04/14 Javascript
EXTJS记事本 当CompositeField遇上RowEditor
2011/07/31 Javascript
JavaScript实现找出字符串中第一个不重复的字符
2014/09/03 Javascript
JavaScript函数详解
2015/02/27 Javascript
JavaScript几种数组去掉重复值的方法推荐
2016/04/12 Javascript
jQuery获取复选框被选中数量及判断选择值的方法详解
2016/05/25 Javascript
漫谈JS引擎的运行机制 你应该知道什么
2016/06/15 Javascript
微信小程序之购物车功能
2020/09/23 Javascript
javascript计算渐变颜色的实例
2017/09/22 Javascript
JavaScript实现浅拷贝与深拷贝的方法分析
2018/07/05 Javascript
详解JavaScript中关于this指向的4种情况
2019/04/18 Javascript
微信小程序如何调用图片接口API并居中显示
2019/06/29 Javascript
VUE实现移动端列表筛选功能
2019/08/23 Javascript
js实现图片上传到服务器和回显
2020/01/19 Javascript
wxPython中文教程入门实例
2014/06/09 Python
Python异常学习笔记
2015/02/03 Python
Python类的定义、继承及类对象使用方法简明教程
2015/05/08 Python
在pandas多重索引multiIndex中选定指定索引的行方法
2018/11/16 Python
Python进阶之全面解读高级特性之切片
2019/02/19 Python
2019 Python最新面试题及答案16道题
2019/04/11 Python
python实现桌面气泡提示功能
2019/07/29 Python
浅谈python中频繁的print到底能浪费多长时间
2020/02/21 Python
详解python算法常用技巧与内置库
2020/10/17 Python
细说NumPy数组的四种乘法的使用
2020/12/18 Python
大学生期末自我鉴定
2014/02/01 职场文书
学习雷锋寄语大全
2014/04/11 职场文书
驾驶员培训方案
2014/05/01 职场文书
承诺函格式模板
2015/01/21 职场文书
个人先进事迹总结
2015/02/26 职场文书
运动会宣传稿100字
2015/07/23 职场文书
Spring-cloud Config Server的3种配置方式
2021/09/25 Java/Android
Mysql的Table doesn't exist问题及解决
2022/12/24 MySQL