您好,登錄后才能下訂單哦!
小編給大家分享一下Django如何實現(xiàn)web端tailf日志文件功能,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
效果圖
接著我們來看下具體的實現(xiàn)過程
技術(shù)實現(xiàn)
所有代碼均基于以下軟件版本:
python==3.6.3
django==2.2
channels==2.1.7
celery==4.3.0
celery4在windows下支持不完善,所以請 在linux下運行 測試
日志數(shù)據(jù)定義
我們只希望用戶能夠查詢固定的幾個日志文件,就不是用數(shù)據(jù)庫僅借助settings.py文件里寫全局變量來實現(xiàn)數(shù)據(jù)存儲
在settings.py里添加一個叫 TAILF 的變量,類型為字典,key標識文件的編號,value標識文件的路徑
TAILF = { 1: '/ops/coffee/error.log', 2: '/ops/coffee/access.log', }
基礎(chǔ)Web頁面搭建
假設(shè)你已經(jīng)創(chuàng)建好了一個叫tailf的app,并添加到了settings.py的INSTALLED_APPS中,app的目錄結(jié)構(gòu)大概如下
tailf - migrations - __init__.py - __init__.py - admin.py - apps.py - models.py - tests.py - views.py
依然先構(gòu)建一個標準的Django頁面,相關(guān)代碼如下
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'), ]
因為我們規(guī)定只有通過登錄的用戶才能查看日志,所以引入Django自帶的LoginView,logoutView幫助我們快速構(gòu)建Login,Logout功能
指定了登錄模板使用 login.html ,它就是一個標準的登錄頁面,post傳入username和password兩個參數(shù)即可,不貼代碼了
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="">選擇要監(jiān)聽的日志</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="開始監(jiān)聽"/><br/> </div> <div class="col-sm-2"> <input class="btn btn-warning btn-block" type="button" onclick="goclose()" value="終止監(jiān)聽"/><br/> </div> <div class="col-sm-12"> <textarea class="form-control" id="chat-log" disabled rows="20"></textarea> </div> {% endblock %}
前端拿到 TAILF 后通過循環(huán)的方式填充到select選擇框下,因為數(shù)據(jù)是字典格式,使用 logDict.items 的方式可以循環(huán)出字典的key和value
這樣一個日志監(jiān)聽頁面就完成了,但還無法實現(xiàn)日志的監(jiān)聽,繼續(xù)往下
集成Channels實現(xiàn)WebSocket
日志監(jiān)聽功能主要的設(shè)計思路就是頁面跟后端服務(wù)器建立websocket長連接,后端通過celery異步執(zhí)行while循環(huán)不斷的讀取日志文件然后發(fā)送到websocket的channel里,實現(xiàn)頁面上的實時顯示
接著我們來集成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,區(qū)別于上一篇中介紹的寫路由文件路徑的方式
頁面需要將監(jiān)聽的日志文件傳遞給后端,我們使用routing正則 P<id>\d+ 傳文件ID給后端程序,后端程序拿到ID之后根據(jù)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): # 中止執(zhí)行中的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,彼此互不影響,可以隨意終止任何一個監(jiān)聽日志的請求
connect
我們知道 self.scope 類似于Django中的request,記錄了豐富的請求信息,通過 self.scope["url_route"]["kwargs"]["id"]
取出routing中正則匹配的日志ID
然后將 id 和 channel_name 傳遞給celery的任務(wù)函數(shù)tailf,tailf根據(jù) id 取到日志文件的路徑,然后循環(huán)文件,將新內(nèi)容根據(jù) channel_name 寫入對應(yīng)channel
disconnect
當websocket連接斷開的時候我們需要終止Celery的Task執(zhí)行,以清除celery的資源占用
終止Celery任務(wù)使用到 revoke 指令,采用如下代碼來實現(xiàn)
self.result.revoke(terminate=True)
注意 self.result 是一個result對象,而非id
參數(shù) terminate=True 的意思是是否立即終止Task,為True時無論Task是否正在執(zhí)行都立即終止,為False(默認)時需要等待Task運行結(jié)束之后才會終止,我們使用了While循環(huán)不設(shè)置為True就永遠不會終止了
終止Celery任務(wù)的另外一種方法是:
from webapp.celery import app app.control.revoke(result.id, terminate=True) send_message
方便我們通過Django的view或者Celery的task調(diào)用給channel發(fā)送消息,官方也比較推薦這種方式
使用Celery異步循環(huán)讀取日志
上邊已經(jīng)集成了Channels實現(xiàn)了WebSocket,但connect函數(shù)中的celery任務(wù) tailf 還沒有實現(xiàn),下邊來實現(xiàn)它
關(guān)于Celery的詳細內(nèi)容可以看這篇文章: 《Django配置Celery執(zhí)行異步任務(wù)和定時任務(wù)》 ,本文就不介紹集成使用以及細節(jié)原理,只講一下任務(wù)task
task實現(xiàn)代碼如下:
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": "微信公眾號【運維咖啡吧】原創(chuàng) 版權(quán)所有 " + str(line) } ) else: time.sleep(0.5) except Exception as e: print(e)
這里邊主要涉及到Channels中另一個非常重要的點: 從Channels的外部發(fā)送消息給Channel
其實 上篇文章 中檢查通道層是否能夠正常工作的時候使用的方法就是從外部給Channel通道發(fā)消息的示例,本文的具體代碼如下
async_to_sync(channel_layer.send)( channel_name, { "type": "send.message", "message": "微信公眾號【運維咖啡吧】原創(chuàng) 版權(quán)所有 " + str(line) } )
channel_name對應(yīng)于傳遞給這個任務(wù)的channel_name,發(fā)送消息給這個名字的channel
type對應(yīng)于我們Channels的TailfConsumer類中的 send_message 方法,將方法中的 _ 換成 . 即可
message就是要發(fā)送給這個channel的具體信息
上邊是發(fā)送給單Channel的情況,如果是需要發(fā)送到Group的話需要使用如下代碼
async_to_sync(channel_layer.group_send)( group_name, { 'type': 'chat.message', 'message': '歡迎關(guān)注公眾號【運維咖啡吧】' } )
只需要將發(fā)送單channel的 send 改為 group_send , channel_name 改為 group_name 即可
需要特別注意的是: 使用了channel layer之后一定要通過async_to_sync來異步執(zhí)行
頁面添加WebSocket支持
后端功能都已經(jīng)完成,我們最后需要添加前端頁面支持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); // 跳轉(zhuǎn)到頁面底部 $('#chat-log').scrollTop($('#chat-log')[0].scrollHeight); }; chatSocket.onerror = function(e) { toastr.error('服務(wù)端連接異常!') }; chatSocket.onclose = function(e) { toastr.error('websocket已關(guān)閉!') }; } else { toastr.warning('請選擇要監(jiān)聽的日志文件') } }
上一篇文章 中有詳細介紹過websocket的消息類型,這里不多介紹了
至此我們一個日志監(jiān)聽頁面完成了,包含了完整的監(jiān)聽功能,但還無法終止,接著看下面的內(nèi)容
Web頁面主動斷開WebSocket
web頁面上“終止監(jiān)聽”按鈕的主要邏輯就是觸發(fā)WebSocket的onclose方法,從而可以觸發(fā)Channels后端consumer的 disconnect 方法,進而終止Celery的循環(huán)讀取日志任務(wù)
前端頁面通過 .close() 可以直接觸發(fā)WebSocket關(guān)閉,當然你如果直接關(guān)掉頁面的話也會觸發(fā)WebSocket的onclose消息,所以不用擔心Celery任務(wù)無法結(jié)束的問題
function goclose() { console.log(window.chatSocket); window.chatSocket.close(); window.chatSocket.onclose = function(e) { toastr.success('已終止日志監(jiān)聽!') }; }
至此我們包含完善功能的Tailf日志監(jiān)聽、終止頁面就全部完成了
以上是“Django如何實現(xiàn)web端tailf日志文件功能”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。