Django websocket之web端实时查看日志实践案例
这是Django Channels系列文章的第二篇,以web端实现tailf的案例讲解Channels的具体使用以及跟Celery的结合
通过上一篇《Django使用Channels实现WebSocket--上篇》的学习应该对Channels的各种概念有了清晰的认知,可以顺利的将Channels框架集成到自己的Django项目中实现WebSocket了,本篇文章将以一个Channels+Celery实现web端tailf功能的例子更加深入的介绍Channels
先说下我们要实现的目标:所有登录的用户可以查看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日志监听、终止页面就全部完成了
写在最后
两篇文章结束不知道你是否对Channels有了更深一步的了解,能够操刀上手将Channels用在自己的项目中,实现理想的功能。个人觉得Channels的重点和难点在于对channel layer的理解和运用,真正的理解了并能熟练运用,相信你一定能够举一反三完美实现更多需求。最后如果对本文的demo源码感兴趣可以关注微信公众号【运维咖啡吧】后台回复小二加我微信向我索取,一定有求必应
相关文章推荐阅读:
Django websocket之web端实时查看日志实践案例的更多相关文章
- Linux 下实时查看日志
Linux 下实时查看日志 cat /var/log/*.log 如果日志在更新,如何实时查看 tail -f /var/log/messages 还可以使用 watch -d -n 1 cat /v ...
- Comet技术详解:基于HTTP长连接的Web端实时通信技术
前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...
- 小程序 web 端实时运行工具
微信小程序 web 端实时运行工具 https://chemzqm.github.io/wept/
- tail -f 实时查看日志文件 linux查看日志后100行
tail -f 实时查看日志文件 tail -f 日志文件logtail - 100f 实时查看日志文件 后一百行tail -f -n 100 catalina.out linux查看日志后100行搜 ...
- LogStation 支持浏览器实时查看日志
我们在logback 分布式日志汇总中已经将日志输出到了all.logs,LogStation支持浏览器实时查看日志,适合研发和运维彼此独立的场景:研发没有服务器权限,却想看日志实时输出.再配合ngi ...
- nohup 后台运行脚本,且可以实时查看日志
-u加在python上 python命令加上-u(unbuffered)参数后会强制其标准输出也同标准错误一样不通过缓存直接打印到屏幕. 这是因为python的缓存机制所决定的 如果是使用 nohup ...
- Linux 实时查看日志文件动态内容
tailf 27.log | grep 'Classcomment/praise' 'Classcomment/praise' 接口名:查看请求固定接口的时间,实时 tai ...
- Linux实时查看日志的四种命令详解
转至:https://blog.csdn.net/qq_33223299/article/details/93773989 如何在Linux中实时查看日志文件的内容?那么有很多实用程序可以帮助用户在文 ...
- Linux实时查看日志,访问前10IP 和相关命令
Nginx日志分析可以获得很多有用的信息,现在来试试最基本的,获取最多访问的前10个IP地址及访问次数. 既然是统计,那么awk是必不可少的,好用而高效. 命令如下: awk '{a[$1] += 1 ...
随机推荐
- 记一次腾讯IEG面试失败经历
如果这是一次成功的经历,估计浏览量不会低.无奈本人能力有限,而且一直在实习,准备时间与面试经验有限导致此次失败,不过,失败也是一种宝贵的经验,我希望也相信这里能给大家一些比较珍贵的经验,废话不多说,上 ...
- 大白话5分钟带你走进人工智能-第30节集成学习之Boosting方式和Adaboost
目录 1.前述: 2.Bosting方式介绍: 3.Adaboost例子: 4.adaboost整体流程: 5.待解决问题: 6.解决第一个问题:如何获得不同的g(x): 6.1 我们看下权重与函数的 ...
- linux下svn安装
1.环境centos6.4 2.安装svnyum -y install subversion 3.配置 建立版本库目录mkdir /www/svndata svnserve -d -r /www/sv ...
- MxNet 模型转Tensorflow pb模型
用mmdnn实现模型转换 参考链接:https://www.twblogs.net/a/5ca4cadbbd9eee5b1a0713af 安装mmdnn pip install mmdnn 准备好mx ...
- 搭建CentOS 7本地源仓库
CentOS 7离线包及其依赖 推荐使用yumdownloader --resolve --destdir=path python-pip,--resolve下载所有依赖,--destdir指定软件包 ...
- JavaScript原型和原型链( prototype 与 __proto__ )
一.函数对象 所有引用类型(函数.数组.对象)都拥有__proto__属性(隐式原型) 所有函数拥有 prototype 属性(显式原型)(仅限函数) 原型对象:拥有 prototype 属性的对象, ...
- Spring 入门程序
1.0 导包的时候要注意: 以上的第一个是.class文件 以上的第二个是文件的解释性页面. 以上的第三个是.java文件 2.0 配置文件需要导入依赖(有dtd 依赖,也有xsd依赖) ² 从be ...
- Android使用Camera2获取预览数据
一.Camera2简介 Camera2是Google在Android 5.0后推出的一个全新的相机API,Camera2和Camera没有继承关系,是完全重新设计的,且Camera2支持的功能也更加丰 ...
- VS2008 专业版试用到期破解 【转】
对于在win7内核下的vs2008破解,和在xp内核系统下的破解是不同的.传统(XP)的破解方式: 一.先安装试用版,然后在“添加或删除程序”里找到VS2008,点“更改/删除”就会看到一个输入序列号 ...
- umeditor 上传图片 相对路径的设置和保存
此篇文章仅献给已实现百度富文本编辑器,想要设置相对路径并保存到数据库,方便以后项目迁移. 使用的版本为1.2.3,适用图片上传中点击以及拖拽功能. //保存图片相对地址的设置 //1.请 ...