动态获取容器日志
前段时间做了一个web端的docker可视化管理平台,采用vue + python flask 前后端分离实现。有一个功能是弹框动态显示容器日志,等同于命令docker logs --tail=20 -f containerName
.
进行docker管理,后端实际是使用docker官方提供的docker-py。最开始的想法还是通过http接口请求来实现,但tcp三次握手建立连接,四次挥手断开连接,一次http请求就结束了,无法动态的获取后端数据。如果保持长连接,在性能方面应该也有不小的损耗。
最后选择使用WebSocket来实现。WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。使用WebSocket,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯.
使用WebSocket实现功能
由于使用的框架是vue和flask,这里对应使用了Vue-Socket.io和flask-socketio。
flask-socketio
这个库更方便的使flask框架提供WebSocket服务端功能。
在程序入口文件中,使用socket对象来包装app并代替它。
from flask_socketio import SocketIO
socket_io = SocketIO()
socket_io.init_app(app,cors_allowed_origins='*')
if __name__ == '__main__':
socket_io.run(app,debug=app.config['DEBUG'],host=app.config['HOST'],port=app.config['PORT'])
这里需要特别关注的是如何解决跨域问题。在前后分离的项目中,一般都是由后端解决跨域问题。flask中http接口使用CORS(app, supports_credentials=True)
来解决,但是WebSocket接口想要解决跨域问题,则要在实例化socketio对象时添加cors_allowed_origins
参数。这个参数支持传入字符串或列表类型的数据,传入值是IP,如果允许所有IP,则传入*。
WebSocket服务端接口
@socket_io.on('logs')
def logs(data):
host = data.get('host')
name = data.get('name')
certification = get_certification(host)
# 判断是否鉴权
if certification:
cert,key = cert_file_path(host)
tls_config = docker.tls.TLSConfig(client_cert=(cert, key),verify=False)
client = docker.DockerClient(base_url='tcp://' + host + ':2376', tls=tls_config)
else:
client = docker.DockerClient(base_url='tcp://' + host + ':2375')
c = client.containers.get(name)
for line in c.logs(stream=True, tail=20, follow=True):
print(line.decode('utf-8').strip())
emit(host + name, {'name': name, 'msg': line.decode('utf-8').strip()})
在socket对象上使用on来监听客户端发送的事件,如代码中的logs,这是自定义的,服务端在发送消息时要写logs。这里获取到客户端发来的host和容器名称,生成docker client,使用client获取容器再获取日志。获取到的日志是一个生成器。循环生成器获取日志字符串并使用emit方法发送给客户端。emit方法发送时指定了emit事件,客户端同样定义了这个事件,并监听事件获取消息。
vue-socket.io
与vue集成,使得WebSocket客户端实现变得简单。
在main.js 中
import VueSocketIO from 'vue-socket.io'
Vue.use(new VueSocketIO({
debug: true,
// 服务器端地址
connection: 'http://127.0.0.1:5005/',
}))
业务代码
//启动查看 、继续查看日志
startLogs() {
this.logPrint = true
if (!(this.hasLogs.indexOf(this.host + this.logContainer) > -1)) {
this.hasLogs.push(this.host + this.logContainer)
this.$socket.emit('logs', {
host: this.host,
name: this.logContainer,
})
}
this.sockets.subscribe(this.host + this.logContainer, (data) => {
console.log(data.name)
if (data.name === this.logContainer) {
this.logs = this.logs + data.msg + '<br>'
const div = this.$refs.elscrollbar.$refs.wrap
// 滑动滚动条到最底部
this.$nextTick(() => {
div.scrollTop = div.scrollHeight
})
}
})
},
//暂停日志
pauseLogs() {
this.logPrint = false
this.sockets.unsubscribe(this.host + this.logContainer)
},
打开日志弹框和点击继续按钮调用startLogs方法,在startLogs中进行了判断,判断日志是否之前被查看过,如果没有查看,则发送服务器和容器名称消息给服务端,并订阅对应的获取日志事件。如果已经被查看则直接订阅获取日志事件。暂停日志则是点击弹窗中的暂停按钮来取消订阅的获取日志事件。这里需要注意的时关闭弹窗时也需要调用暂停方法,否则在下次打开弹框时会输出双倍的日志,因为这样订阅了2次。