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遍历删除字典里值为空的元素报错问题
Sep 11 Python
python执行使用shell命令方法分享
Nov 08 Python
Python使用paramiko操作linux的方法讲解
Feb 25 Python
python中的数据结构比较
May 13 Python
简单了解python的break、continue、pass
Jul 08 Python
使用python将最新的测试报告以附件的形式发到指定邮箱
Sep 20 Python
python各层级目录下import方法代码实例
Jan 20 Python
Python读入mnist二进制图像文件并显示实例
Apr 24 Python
python os模块常用的29种方法使用详解
Jun 02 Python
Python分类测试代码实例汇总
Jul 23 Python
Django修改app名称和数据表迁移方案实现
Sep 17 Python
python 如何在 Matplotlib 中绘制垂直线
Apr 02 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
Mysql和网页显示乱码解决方法集锦
2008/03/27 PHP
PHP图片验证码制作实现分享(全)
2012/05/10 PHP
thinkPHP3.1验证码的简单实现方法
2016/04/22 PHP
PHP的openssl加密扩展使用小结(推荐)
2016/07/18 PHP
PHP会话操作之cookie用法分析
2016/09/28 PHP
PHP中的多种加密技术及代码示例解析
2016/10/20 PHP
PHP实现查询手机归属地的方法详解
2017/04/28 PHP
PHP7.3.10编译安装教程
2019/10/08 PHP
Code:findPosX 和 findPosY
2006/12/20 Javascript
asp.net 30分钟掌握无刷新 Repeater
2011/09/16 Javascript
基于jquery的防止大图片撑破页面的实现代码(立即缩放)
2011/10/24 Javascript
Javascript/Jquery——简单定时器的多种实现方法
2013/07/03 Javascript
深入分析escape()、encodeURI()、encodeURIComponent()的区别及示例
2014/08/04 Javascript
javascript关于open.window子页面执行完成后刷新父页面的问题分析
2015/04/27 Javascript
页面向下滚动ajax获取数据的实现方法(兼容手机)
2016/05/24 Javascript
JavaScript版经典游戏之扫雷游戏完整示例【附demo源码下载】
2016/12/12 Javascript
jQuery焦点图轮播效果实现方法
2016/12/19 Javascript
Vue.js实例方法之生命周期详解
2017/07/03 Javascript
浅谈JavaScript作用域和闭包
2017/09/18 Javascript
ES6 javascript中Class类继承用法实例详解
2017/10/30 Javascript
Vue render渲染时间戳转时间,时间转时间戳及渲染进度条效果
2018/07/27 Javascript
vue+AI智能机器人回复功能实现
2020/07/16 Javascript
在Vue中获取自定义属性方法:data-id的实例
2020/09/09 Javascript
使用Vant完成DatetimePicker 日期的选择器操作
2020/11/12 Javascript
详解Django配置JWT认证方式
2020/05/09 Python
python Socket网络编程实现C/S模式和P2P
2020/06/22 Python
基于python判断字符串括号是否闭合{}[]()
2020/09/21 Python
纯CSS3实现地球自转实现代码(图文教程附送源码)
2012/12/26 HTML / CSS
印度尼西亚最大和最全面的网络商城:Blibli.com
2017/10/04 全球购物
公司财务总监岗位职责
2013/12/14 职场文书
机关单位动员会主持词
2014/03/20 职场文书
大学生学雷锋活动总结
2014/06/26 职场文书
信用卡工作证明范本
2015/06/19 职场文书
开学典礼校长致辞
2015/07/29 职场文书
nginx 多个location转发任意请求或访问静态资源文件的实现
2021/03/31 Servers
Nginx配置文件详解以及优化建议指南
2021/09/15 Servers